project-guide 2.7.0__tar.gz → 2.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. {project_guide-2.7.0 → project_guide-2.7.1}/CHANGELOG.md +21 -0
  2. {project_guide-2.7.0 → project_guide-2.7.1}/PKG-INFO +1 -1
  3. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/features.md +9 -1
  4. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/project-essentials.md +9 -3
  5. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/stories.md +47 -1
  6. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/tech-spec.md +16 -6
  7. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/cli.py +52 -30
  8. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/version.py +1 -1
  9. {project_guide-2.7.0 → project_guide-2.7.1}/pyproject.toml +1 -1
  10. {project_guide-2.7.0 → project_guide-2.7.1}/tests/test_cli.py +56 -21
  11. {project_guide-2.7.0 → project_guide-2.7.1}/.github/FUNDING.yml +0 -0
  12. {project_guide-2.7.0 → project_guide-2.7.1}/.github/dependabot.yml +0 -0
  13. {project_guide-2.7.0 → project_guide-2.7.1}/.github/workflows/ci.yml +0 -0
  14. {project_guide-2.7.0 → project_guide-2.7.1}/.github/workflows/deploy-docs.yml +0 -0
  15. {project_guide-2.7.0 → project_guide-2.7.1}/.github/workflows/publish.yml +0 -0
  16. {project_guide-2.7.0 → project_guide-2.7.1}/.github/workflows/test.yml +0 -0
  17. {project_guide-2.7.0 → project_guide-2.7.1}/.gitignore +0 -0
  18. {project_guide-2.7.0 → project_guide-2.7.1}/.project-guide.yml +0 -0
  19. {project_guide-2.7.0 → project_guide-2.7.1}/.pyve/config +0 -0
  20. {project_guide-2.7.0 → project_guide-2.7.1}/.tool-versions +0 -0
  21. {project_guide-2.7.0 → project_guide-2.7.1}/CONTRIBUTING.md +0 -0
  22. {project_guide-2.7.0 → project_guide-2.7.1}/LICENSE +0 -0
  23. {project_guide-2.7.0 → project_guide-2.7.1}/README.md +0 -0
  24. {project_guide-2.7.0 → project_guide-2.7.1}/SECURITY.md +0 -0
  25. {project_guide-2.7.0 → project_guide-2.7.1}/docs/site/.gitignore +0 -0
  26. {project_guide-2.7.0 → project_guide-2.7.1}/docs/site/about/changelog.md +0 -0
  27. {project_guide-2.7.0 → project_guide-2.7.1}/docs/site/about/license.md +0 -0
  28. {project_guide-2.7.0 → project_guide-2.7.1}/docs/site/developer-guide/contributing.md +0 -0
  29. {project_guide-2.7.0 → project_guide-2.7.1}/docs/site/developer-guide/development.md +0 -0
  30. {project_guide-2.7.0 → project_guide-2.7.1}/docs/site/developer-guide/testing.md +0 -0
  31. {project_guide-2.7.0 → project_guide-2.7.1}/docs/site/getting-started.md +0 -0
  32. {project_guide-2.7.0 → project_guide-2.7.1}/docs/site/images/project-guide-banner-landing.png +0 -0
  33. {project_guide-2.7.0 → project_guide-2.7.1}/docs/site/images/project-guide-header-readme.png +0 -0
  34. {project_guide-2.7.0 → project_guide-2.7.1}/docs/site/index.html +0 -0
  35. {project_guide-2.7.0 → project_guide-2.7.1}/docs/site/user-guide/commands.md +0 -0
  36. {project_guide-2.7.0 → project_guide-2.7.1}/docs/site/user-guide/configuration.md +0 -0
  37. {project_guide-2.7.0 → project_guide-2.7.1}/docs/site/user-guide/install-options.md +0 -0
  38. {project_guide-2.7.0 → project_guide-2.7.1}/docs/site/user-guide/modes.md +0 -0
  39. {project_guide-2.7.0 → project_guide-2.7.1}/docs/site/user-guide/overrides.md +0 -0
  40. {project_guide-2.7.0 → project_guide-2.7.1}/docs/site/user-guide/workflow.md +0 -0
  41. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/.archive/phase-j-modes-plan.md +0 -0
  42. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/.archive/phase-k-release-lifecycle-plan.md +0 -0
  43. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/.archive/phase-l-no-input-init-plan.md +0 -0
  44. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/.archive/phase-m-project-essentials-plan.md +0 -0
  45. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/.archive/phase-n-mode-naming-cli-memory-plan.md +0 -0
  46. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/.archive/phase-o-pyve-quiet-embedding-subplan.md +0 -0
  47. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/.archive/phase-o-quiet-non-interactive-embedding-feature.md +0 -0
  48. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/.archive/project-guide-no-input-spec.md +0 -0
  49. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/.archive/stories-v1.3.1.md +0 -0
  50. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/.archive/stories-v2.0.20.md +0 -0
  51. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/.archive/stories-v2.3.9.md +0 -0
  52. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/.archive/stories-v2.4.19.md +0 -0
  53. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/.archive/stories-v2.5.15.md +0 -0
  54. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/.archive/ux-problems-v2.0.10.md +0 -0
  55. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/brand-descriptions.md +0 -0
  56. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/concept.md +0 -0
  57. {project_guide-2.7.0 → project_guide-2.7.1}/docs/specs/phase-p-auto-heal-plan.md +0 -0
  58. {project_guide-2.7.0 → project_guide-2.7.1}/mkdocs.yml +0 -0
  59. {project_guide-2.7.0 → project_guide-2.7.1}/project-guide-old-template.md +0 -0
  60. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/__init__.py +0 -0
  61. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/__main__.py +0 -0
  62. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/actions.py +0 -0
  63. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/config.py +0 -0
  64. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/exceptions.py +0 -0
  65. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/metadata.py +0 -0
  66. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/render.py +0 -0
  67. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/runtime.py +0 -0
  68. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/stories.py +0 -0
  69. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/sync.py +0 -0
  70. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/.project-guide.yml.template +0 -0
  71. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/.metadata.yml +0 -0
  72. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/README.md +0 -0
  73. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/developer/best-practices-guide.md +0 -0
  74. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/developer/brand-descriptions-guide.md +0 -0
  75. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/developer/codecov-setup-guide.md +0 -0
  76. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/developer/debug-guide.md +0 -0
  77. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/developer/landing-page-guide.md +0 -0
  78. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/developer/production-github-guide.md +0 -0
  79. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/developer/project-guide.md +0 -0
  80. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/developer/python-editable-install.md +0 -0
  81. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/artifacts/brand-descriptions.md +0 -0
  82. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/artifacts/concept.md +0 -0
  83. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/artifacts/features.md +0 -0
  84. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/artifacts/project-essentials.md +0 -0
  85. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/artifacts/pyve-essentials.md +0 -0
  86. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/artifacts/stories.md +0 -0
  87. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/artifacts/tech-spec.md +0 -0
  88. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/llm_entry_point.md +0 -0
  89. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/_header-common.md +0 -0
  90. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/_header-cycle.md +0 -0
  91. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/_header-sequence.md +0 -0
  92. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/_phase-letters.md +0 -0
  93. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/archive-stories-mode.md +0 -0
  94. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/code-direct-mode.md +0 -0
  95. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/code-test-first-mode.md +0 -0
  96. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/debug-mode.md +0 -0
  97. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/default-mode.md +0 -0
  98. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/document-brand-mode.md +0 -0
  99. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/document-landing-mode.md +0 -0
  100. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/plan-concept-mode.md +0 -0
  101. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/plan-features-mode.md +0 -0
  102. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/plan-phase-mode.md +0 -0
  103. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/plan-production-phase-mode.md +0 -0
  104. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/plan-stories-mode.md +0 -0
  105. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/plan-tech-spec-mode.md +0 -0
  106. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/refactor-document-mode.md +0 -0
  107. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/refactor-plan-mode.md +0 -0
  108. {project_guide-2.7.0 → project_guide-2.7.1}/project_guide/templates/project-guide/templates/modes/scaffold-project-mode.md +0 -0
  109. {project_guide-2.7.0 → project_guide-2.7.1}/requirements-dev.txt +0 -0
  110. {project_guide-2.7.0 → project_guide-2.7.1}/tests/__init__.py +0 -0
  111. {project_guide-2.7.0 → project_guide-2.7.1}/tests/conftest.py +0 -0
  112. {project_guide-2.7.0 → project_guide-2.7.1}/tests/test_actions.py +0 -0
  113. {project_guide-2.7.0 → project_guide-2.7.1}/tests/test_archive_stories_mode.py +0 -0
  114. {project_guide-2.7.0 → project_guide-2.7.1}/tests/test_config.py +0 -0
  115. {project_guide-2.7.0 → project_guide-2.7.1}/tests/test_integration.py +0 -0
  116. {project_guide-2.7.0 → project_guide-2.7.1}/tests/test_metadata.py +0 -0
  117. {project_guide-2.7.0 → project_guide-2.7.1}/tests/test_purge.py +0 -0
  118. {project_guide-2.7.0 → project_guide-2.7.1}/tests/test_render.py +0 -0
  119. {project_guide-2.7.0 → project_guide-2.7.1}/tests/test_runtime.py +0 -0
  120. {project_guide-2.7.0 → project_guide-2.7.1}/tests/test_sync.py +0 -0
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.7.1] - 2026-05-11
11
+
12
+ Compatibility fix for IDE-integrated LLM @-mention / fuzzy-search. The v2.6.0–v2.7.0 gitignore block used a clean `<target>/**` + `!<target>/go.md` negation pair — correct per the `.gitignore` spec, but **several IDE tools** (Cursor, parts of the VS Code fork ecosystem, certain LSP-based search backends) implement a subset of gitignore semantics that does not honor re-include negation. Those tools applied the broad `**` rule, hid `go.md`, and defeated the IDE-LLM-visibility constraint that's the entire reason `go.md` is tracked. P.l switches to a negation-free explicit-list form so simplistic parsers handle it reliably.
13
+
14
+ ### Changed
15
+ - **`project_guide/cli.py:_build_project_guide_block()`** — rewritten to enumerate the bundled template root (`_get_package_template_dir()`) and emit one anchored `/<target>/<child>[/]` line per top-level entry other than `go.md`, plus a trailing `/<target>/**/*.bak.*` defensive catch-all for top-level backups. The list is dynamic — new top-level files/directories in the bundled tree are picked up automatically by both the writer and the test helper that mirrors it. Default install now writes:
16
+ ```
17
+ # project-guide
18
+ /docs/project-guide/.metadata.yml
19
+ /docs/project-guide/README.md
20
+ /docs/project-guide/developer/
21
+ /docs/project-guide/templates/
22
+ /docs/project-guide/**/*.bak.*
23
+ ```
24
+ - **`project_guide/cli.py`** — `_recognized_block_lines()` replaced with `_is_recognized_block_line(line, target_dir)` predicate. Accepts the v2.7.1+ form (any line anchored at `/<target>/`) plus every prior legacy line (`<target>/**`, `!<target>/go.md`, `<target>/**/*.bak.*`, `<target>/go.md`). Existing v2.6.x/v2.7.0 installs heal cleanly to the v2.7.1 form on `init --force`; no multi-step migration required.
25
+ - **Tests** — `_EXPECTED_GITIGNORE_BLOCK` constant replaced with `_expected_gitignore_block()` helper that mirrors the writer's enumeration; new `test_init_force_rewrites_v261_three_line_block_to_explicit_list` for the v2.6.1/v2.7.0 → v2.7.1 migration path; the foreign-block test now seeds a line not anchored at `/<target>/` so the predicate correctly flags it as foreign.
26
+ - **Docs** — updated the gitignore prose in `docs/specs/features.md`, `docs/specs/tech-spec.md`, and `docs/specs/project-essentials.md` to document the three-version evolution (v2.6.0 → v2.6.1 → v2.7.1) and the "do not simplify back to negation" warning for future maintainers.
27
+
28
+ ### Migration
29
+ None required at the consumer level. The v2.6.x/v2.7.0 negation form and the v2.7.1 explicit-list form produce identical git-tracking outcomes (only `go.md` is tracked). The behavior change is purely in what `_ensure_gitignore_entry()` writes. Consumers whose IDE handles negation correctly may keep their existing block indefinitely; only consumers hitting the IDE bug actually benefit from running `project-guide init --force` to adopt the new form.
30
+
10
31
  ## [2.7.0] - 2026-05-11
11
32
 
12
33
  New top-level `project-guide git-push` command — a thin wrapper over [gitbetter](https://github.com/pointmatic/gitbetter)'s `git-push` that auto-derives the commit subject from the most-recently-completed-and-not-yet-committed story in `docs/specs/stories.md`. Developer-lane convenience only; the LLM still does not initiate commits.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: project-guide
3
- Version: 2.7.0
3
+ Version: 2.7.1
4
4
  Summary: Stay organized and in control with adaptive LLM workflow prompts.
5
5
  Project-URL: Homepage, https://github.com/pointmatic/project-guide
6
6
  Project-URL: Documentation, https://pointmatic.github.io/project-guide/
@@ -376,7 +376,15 @@ The bundled `templates/artifacts/pyve-essentials.md` artifact covers: two-enviro
376
376
  - `.project-guide.yml` is absent (let `init` bootstrap; the hook does not error).
377
377
  - The config fails to load (schema mismatch, parse error) — the subcommand surfaces the error with its own guidance.
378
378
 
379
- **Inverted gitignore policy.** `init`'s gitignore writer produces a canonical 3-line block under a `# project-guide` header: ignore everything under `target_dir` *except* `go.md` (tightened from the original 4-line v2.6.0 form in v2.6.1 — the explicit `.bak.*` line was redundant with the broader `**` rule). The remaining template tree is bundled static data that `heal` repopulates on first invocation. This eliminates the ~35-file install footprint from consumer-repo `git status` and PR reviews. Consumers migrating from a pre-Phase-P install run `project-guide init --force` to refresh the gitignore block; `git rm --cached` is the manual cleanup for previously tracked files. v2.6.0 installs heal to the v2.6.1 3-line form on the next `init --force`.
379
+ **Inverted gitignore policy.** `init`'s gitignore writer produces a canonical block under a `# project-guide` header that ignores everything under `target_dir` *except* `go.md`. The block has gone through three shapes (P.d P.j P.l):
380
+
381
+ - **v2.6.0 (P.d):** 4-line negation form (`<target>/**` + `!<target>/go.md` + redundant `<target>/**/*.bak.*`).
382
+ - **v2.6.1 (P.j):** 3-line negation form — dropped the redundant `.bak.*` line.
383
+ - **v2.7.1 (P.l):** **negation-free explicit-list form** — lists every top-level entry under `target_dir` other than `go.md`, plus a `<target>/**/*.bak.*` catch-all for top-level backups. The list is generated dynamically from the bundled template tree, so new top-level files/subdirectories added in future releases are picked up automatically.
384
+
385
+ P.l abandoned the negation form because several IDE-integrated tools (Cursor, parts of the VS Code fork ecosystem, certain LSP-based search backends) implement a subset of `.gitignore` semantics that does not honor re-include negation — they apply the broad `**` rule, hide `go.md` from @-mention / fuzzy-search, and defeat the IDE-LLM-visibility constraint that's the whole reason `go.md` is tracked.
386
+
387
+ Consumers migrating from a pre-Phase-P install run `project-guide init --force` to refresh the gitignore block; `git rm --cached` is the manual cleanup for previously tracked files. Existing v2.6.x/v2.7.0 installs heal to the v2.7.1 explicit-list form on the next `init --force` — every prior shape stays recognized by `_is_recognized_block_line()`. The track-only-`go.md` policy is unchanged across all three shapes; only the syntax that expresses it differs.
380
388
 
381
389
  ### FR-15: Story-Aware `git-push` Wrapper (gitbetter integration)
382
390
 
@@ -63,11 +63,17 @@ The hook is silent in the steady state (no drift → no output). It is recursion
63
63
 
64
64
  **Skip conditions:** the hook returns silently when `PROJECT_GUIDE_HEALING=1` is set, when `.project-guide.yml` is absent (let `init` bootstrap; `heal` would error otherwise), or when config load / drift detection fails. Missing `.project-guide.yml` is a hard error from the **`heal` subcommand itself** but a silent skip from the **hook** — the original subcommand surfaces the missing-config error with its own guidance.
65
65
 
66
- ### Inverted gitignore policy (added v2.6.0, tightened v2.6.1)
66
+ ### Inverted gitignore policy (added v2.6.0, tightened v2.6.1, IDE-compat reshape v2.7.1)
67
67
 
68
- `init`'s gitignore writer produces a canonical 3-line block: ignore everything under `target_dir` *except* `go.md` (Story P.d wrote the inversion; Story P.j / v2.6.1 dropped a redundant `.bak.*` line that the original block carried over from the pre-P.d shape). The remaining template tree is bundled static data that `heal` repopulates on first invocation in a fresh clone, so tracking it in the consumer repo would just add ~35 files of noise to `git status` and PR reviews.
68
+ `init`'s gitignore writer produces a canonical block that ignores everything under `target_dir` *except* `go.md`. The policy has gone through three shapes:
69
69
 
70
- **Idempotent rewrite:** `_ensure_gitignore_entry()` recognizes the v2.6.1 canonical block plus prior forms (the v2.6.0 4-line block, the pre-P.d `.bak.*`-only block, and the legacy `<target>/go.md` line) and rewrites all of them cleanly to the v2.6.1 3-line form. A foreign hand-customized block under a `# project-guide` header is left untouched with a stderr warning; migrate manually or run `init --force`.
70
+ - **v2.6.0 (P.d):** 4-line negation form (`<target>/**` + `!<target>/go.md` + redundant `<target>/**/*.bak.*`).
71
+ - **v2.6.1 (P.j):** 3-line negation form — dropped the redundant `.bak.*` line.
72
+ - **v2.7.1 (P.l):** **negation-free explicit-list form** — lists every top-level entry under `target_dir` other than `go.md`, plus a `<target>/**/*.bak.*` defensive catch-all. The list is dynamically enumerated from the bundled template root at write time, so new top-level files/directories added in future stories are picked up automatically.
73
+
74
+ P.l abandoned negation because several IDE-integrated tools (Cursor, parts of the VS Code fork ecosystem, certain LSP-based search backends) implement a subset of `.gitignore` semantics that does **not** honor re-include negation — they apply the broad `**` rule, hide `go.md` from @-mention / fuzzy-search, and defeat the IDE-LLM-visibility constraint the policy is trying to enforce. **Future maintainers: do not "simplify" back to `**` + `!` — the regression is invisible from git's perspective but breaks the IDE workflow.**
75
+
76
+ **Idempotent rewrite:** `_ensure_gitignore_entry()` uses `_is_recognized_block_line(line, target_dir)` as its "ours-vs-foreign" predicate. It accepts anything anchored at `/<target>/` (the v2.7.1+ form) plus every prior legacy form (v2.6.1 3-line, v2.6.0 4-line, pre-P.d `.bak.*`-only, legacy `<target>/go.md`). Any block whose lines all satisfy the predicate is rewritten cleanly to the current canonical form. A foreign hand-customized block under a `# project-guide` header is left untouched with a stderr warning; migrate manually or run `init --force`.
71
77
 
72
78
  ### IDE-LLM visibility constraint (added v2.6.0)
73
79
 
@@ -34,7 +34,7 @@ Collapse the `docs/project-guide/` install footprint in consumer repos to a sing
34
34
 
35
35
  **Implementation order:** `heal` core (P.a) → auto-hook (P.b) → `--no-input` semantics (P.c) → gitignore template flip (P.d). Production hardening items (P.e–P.h) are independent and can ship in any order. P.i is the doc-only story that bundles the v2.6.0 release.
36
36
 
37
- **Version cadence:** phase-bundled — stories P.a–P.i ran unversioned and shipped together as **v2.6.0** (P.i owned the single bundled bump). **Stories P.j and P.k were added post-bundle** and follow standard per-story cadence: **P.j → v2.6.1** (patch — gitignore-block tightening, a fix-up to P.d), **P.k → v2.7.0** (minor — new `git-push` wrapper command). Phases can be extended after their bundled release, but new stories then follow the standard per-story cadence rather than rejoining the (already-shipped) bundle.
37
+ **Version cadence:** phase-bundled — stories P.a–P.i ran unversioned and shipped together as **v2.6.0** (P.i owned the single bundled bump). **Stories P.j, P.k, and P.l were added post-bundle** and follow standard per-story cadence: **P.j → v2.6.1** (patch — gitignore-block tightening, a fix-up to P.d), **P.k → v2.7.0** (minor — new `git-push` wrapper command), **P.l → v2.7.1** (patch — switch the gitignore block from negation to explicit-list form for IDE compatibility). Phases can be extended after their bundled release, but new stories then follow the standard per-story cadence rather than rejoining the (already-shipped) bundle.
38
38
 
39
39
  ### Story P.a: Heal command with drift detection and create-missing semantics [Done]
40
40
 
@@ -262,6 +262,52 @@ This is a developer-lane convenience command. **The LLM still does not initiate
262
262
  - A parallel `project-guide git-tag` wrapper. gitbetter has one but the project's release process already uses `bump-version` + raw `git tag`, so the value is lower.
263
263
  - Pre-flight `pyve test` / `ruff check` gates before invoking gitbetter. The developer runs those during the story's normal cycle before marking `[Done]`, so re-running them at push time is redundant.
264
264
 
265
+ ### Story P.l: v2.7.1 Negation-free gitignore for IDE LLM compatibility [Done]
266
+
267
+ Switch the canonical `# project-guide` gitignore block from the v2.6.1/v2.7.0 negation form (`<target>/**` + `!<target>/go.md`) to a **negation-free explicit-list** form. Motivation: several IDE-integrated tools (Cursor, parts of the VS Code fork ecosystem, certain LSP-based search backends) implement a subset of `.gitignore` semantics that does not honor re-include negation — they apply the broad `**` rule, hide `go.md` from @-mention and fuzzy-search, and defeat the IDE-LLM-visibility constraint that's the entire reason `go.md` is tracked.
268
+
269
+ The new canonical form lists every gitignored top-level entry explicitly so no negation is required. The list is **dynamically generated** from the bundled template root, so new top-level files or subdirectories added in future stories are picked up automatically — no manual maintenance.
270
+
271
+ - [x] In `project_guide/cli.py:_build_project_guide_block()`: rewrite to enumerate `_get_package_template_dir()` and emit one `/<target>/<child>[/]` line per top-level entry except `go.md`, plus the existing `/<target>/**/*.bak.*` backup-catch-all. New canonical form for the default install layout:
272
+ ```
273
+ # project-guide
274
+ /docs/project-guide/.metadata.yml
275
+ /docs/project-guide/README.md
276
+ /docs/project-guide/developer/
277
+ /docs/project-guide/templates/
278
+ /docs/project-guide/**/*.bak.*
279
+ ```
280
+ Use a leading slash to anchor each rule at repo root; trailing slash on directories. Children sorted for deterministic output.
281
+ - [x] In `project_guide/cli.py`: update the recognized-block check to accept every form we've ever written: *(replaced `_recognized_block_lines()` with a `_is_recognized_block_line(line, target_dir)` predicate — cleaner because the v2.7.1+ "anything anchored at `/<target>/`" rule isn't expressible as a fixed set)*
282
+ - [x] New v2.7.1+ form: any line starting with `/<target>/`
283
+ - [x] v2.6.1 form: `<target>/**`, `!<target>/go.md`
284
+ - [x] v2.6.0 form: `<target>/**`, `!<target>/go.md`, `<target>/**/*.bak.*`
285
+ - [x] pre-P.d form: `<target>/**/*.bak.*` only
286
+ - [x] Legacy `<target>/go.md` line
287
+ - [x] In `tests/test_cli.py`:
288
+ - [x] Compute the expected block from the bundled tree via `_expected_gitignore_block()` helper instead of a hardcoded constant
289
+ - [x] Add `test_init_force_rewrites_v261_three_line_block_to_explicit_list` for the v2.6.1/v2.7.0 → v2.7.1 migration
290
+ - [x] Renamed the v2.6.0 → v2.7.1 migration test to `test_init_force_rewrites_v260_four_line_block_to_explicit_list` (was the v2.6.1-form rewrite test)
291
+ - [x] Foreign-block warning test updated to use a line not anchored at `/<target>/` so the new predicate flags it correctly
292
+ - [x] Idempotency test passes against the new canonical form (the seed builds from `_expected_gitignore_block()`)
293
+ - [x] All prior P.j/P.d migration tests continue to pass
294
+ - [x] Doc updates (bundled in this story):
295
+ - [x] `docs/specs/features.md` FR-14: amended the "Inverted gitignore policy" paragraph with the three-version evolution (v2.6.0 → v2.6.1 → v2.7.1) and the IDE-compat rationale
296
+ - [x] `docs/specs/tech-spec.md` § `.gitignore Management`: replaced the negation code block with the explicit-list form, added a "Why explicit list instead of `**` + `!go.md`?" paragraph plus an explicit "do not simplify back" warning, and a "Why the trailing `<target>/**/*.bak.*` line?" paragraph
297
+ - [x] `docs/specs/project-essentials.md`: amended the "Inverted gitignore policy" sub-section to cover the v2.7.1 form change and the "future maintainers: do not simplify back to `**` + `!`" warning
298
+ - [x] `README.md`: no user-facing prose changes needed (Quick Start footnote still reads correctly — "ignored except go.md")
299
+ - [x] Bump `project_guide/version.py`: `__version__ = "2.7.1"`
300
+ - [x] Bump `pyproject.toml`: `version = "2.7.1"`
301
+ - [x] Add `## [2.7.1] - <date>` CHANGELOG entry framed as a compatibility fix
302
+
303
+ **Migration:** none required by consumers. The new and old blocks produce identical git-tracking outcomes (only `go.md` is tracked). The behavior change is purely in what `_ensure_gitignore_entry()` writes. Existing v2.6.x/v2.7.0 installs heal to the v2.7.1 form on `init --force` because all prior shapes remain recognized. Consumers whose IDE handles negation correctly can keep their existing block indefinitely — only consumers hitting the IDE bug actually need to migrate.
304
+
305
+ **Why dynamic enumeration:** hardcoding the install footprint in `_build_project_guide_block()` means every new top-level template file or subdirectory requires a writer-code update. Enumerating `_get_package_template_dir()` at write time keeps the canonical block in sync with the bundled tree automatically. The trade-off is that the writer now reads from the package — not a real concern since `init` already does the same to copy the template tree.
306
+
307
+ **Out of scope:**
308
+ - An opt-back-in flag (`--gitignore-style=negation`). YAGNI until someone asks; the new form is strictly better for the documented IDE-LLM-visibility constraint.
309
+ - A `project-guide check` integrity rule that detects "consumer has tracked-but-should-be-ignored files under `target_dir`". Defer until there's a second integrity rule worth shipping (see Future > Integrity & Validation).
310
+
265
311
  ---
266
312
 
267
313
  ## Future
@@ -128,7 +128,7 @@ project-guide/
128
128
  | `purge` | Remove all project-guide files with confirmation |
129
129
 
130
130
  **Key functions:**
131
- - `_ensure_gitignore_entry(target_dir)` — writes the canonical `# project-guide` block: ignore everything under `target_dir` except `go.md` (3-line form as of P.j / v2.6.1). Idempotent. Recognized prior blocks (pre-P.d `.bak.*`-only form, v2.6.0 4-line form with the redundant `.bak.*` line, legacy `<target>/go.md` line) are rewritten cleanly to the v2.6.1 3-line form; foreign hand-customized content under a `# project-guide` header is left alone with a stderr warning.
131
+ - `_ensure_gitignore_entry(target_dir)` — writes the canonical `# project-guide` block: ignore everything under `target_dir` except `go.md` (negation-free explicit-list form as of P.l / v2.7.1, dynamically enumerated from the bundled template root). Idempotent. Recognized prior blocks (pre-P.d `.bak.*`-only form, v2.6.0 4-line form, v2.6.1/v2.7.0 3-line negation form, legacy `<target>/go.md` line) are rewritten cleanly to the v2.7.1 explicit-list form; foreign hand-customized content under a `# project-guide` header is left alone with a stderr warning. The recognized-line check is `_is_recognized_block_line(line, target_dir)` — accepts anything anchored at `/<target>/` plus the legacy negation entries.
132
132
  - `_copy_template_tree(src, dest, force)` — recursive copy preserving structure
133
133
  - `_migrate_config_if_needed()` — renames legacy `.project-guides.yml`
134
134
  - `_apply_heal(config, config_path)` — apply pending template syncs and re-render `go.md`. Sets `PROJECT_GUIDE_HEALING=1` in `os.environ` before doing any writes so nested subprocess invocations don't re-enter the auto-hook.
@@ -270,18 +270,28 @@ class ModeDefinition:
270
270
 
271
271
  ### `.gitignore` Management
272
272
 
273
- `init` writes a canonical 3-line block under a `# project-guide` comment header (Story P.d, tightened in P.j / v2.6.1):
273
+ `init` writes a canonical **negation-free explicit-list** block under a `# project-guide` comment header (Story P.d P.j P.l). For the default install layout the block is:
274
+
274
275
  ```
275
276
  # project-guide
276
- docs/project-guide/**
277
- !docs/project-guide/go.md
277
+ /docs/project-guide/.metadata.yml
278
+ /docs/project-guide/README.md
279
+ /docs/project-guide/developer/
280
+ /docs/project-guide/templates/
281
+ /docs/project-guide/**/*.bak.*
278
282
  ```
279
283
 
284
+ The list is **dynamically generated** at write time by enumerating the bundled template root (`_get_package_template_dir()`) and emitting one anchored line per top-level child other than `go.md`. New top-level files or subdirectories added in future releases are picked up automatically — no manual writer update required.
285
+
280
286
  **Why this shape:** every file under `target_dir` except `go.md` is bundled static data that `heal` (FR-14) repopulates on first invocation, so tracking the full template tree in the consumer repo would just add ~35 files of noise to `git status` and PR reviews. `go.md` itself **must remain tracked** because IDE-integrated LLMs (Cursor, Claude Code, etc.) typically hide gitignored files from the LLM's view, and the LLM's instruction to `Read docs/project-guide/go.md` requires the file to be visible. The repo-history value of `go.md` is incidental — the file churns on every mode switch — and that churn is the acceptable cost for LLM visibility.
281
287
 
282
- **Why not the explicit `.bak.*` line that v2.6.0 shipped?** It was carried over from the pre-P.d block during the policy inversion but is functionally redundant: the `<target>/**` rule already ignores backups produced by sync/heal under that subtree. P.j dropped the line; existing v2.6.0 installs heal cleanly to the 3-line form on `init --force` because `_recognized_block_lines()` still lists the v2.6.0 entry.
288
+ **Why explicit list instead of `**` + `!go.md`?** The cleaner negation form (used in v2.6.0–v2.7.0) is correct per the `.gitignore` spec, but several IDE-integrated tools (Cursor, parts of the VS Code fork ecosystem, certain LSP-based search backends) implement a subset of `.gitignore` semantics that does **not** honor re-include negation — they apply the broad `**` rule, hide `go.md` from @-mention search, and defeat the IDE-LLM-visibility constraint the policy is trying to enforce. P.l (v2.7.1) switched to the explicit-list form so no negation is required; tools with simplistic parsers handle anchored line-per-entry patterns reliably. Future maintainers: do **not** "simplify" back to `**` + `!` — the regression is invisible from git's perspective but breaks the IDE workflow that motivates tracking `go.md` in the first place.
289
+
290
+ **Why the trailing `<target>/**/*.bak.*` line?** Defensive coverage for backup files that `apply_file_update()` writes next to top-level synced files (e.g., `<target>/README.md.bak.<timestamp>`). Subdirectory backups are already covered by the per-directory entries; this catch-all is a simple recursive glob with no negation, so the IDEs handle it cleanly.
291
+
292
+ **Existing-block detection:** `_ensure_gitignore_entry()` is idempotent. The recognized-line predicate `_is_recognized_block_line(line, target_dir)` accepts any line starting with `/<target>/` (the v2.7.1+ anchor) plus the legacy negation-form lines (`<target>/**`, `!<target>/go.md`, `<target>/**/*.bak.*`, `<target>/go.md`). A block whose every non-empty line satisfies the predicate is rewritten cleanly to the current canonical form; any line that fails the predicate marks the block as hand-customized — the writer leaves it untouched and emits a stderr warning. A `.gitignore` with no `# project-guide` header gets the canonical block appended (separated by a blank line).
283
293
 
284
- **Existing-block detection:** `_ensure_gitignore_entry()` is idempotent. A recognized prior block (the v2.6.1 canonical form, the v2.6.0 4-line form, or the pre-P.d `.bak.*`-only form) is rewritten cleanly to the v2.6.1 3-line form. A foreign hand-customized block under a `# project-guide` header is left untouched with a stderr warning. A `.gitignore` with no `# project-guide` header gets the canonical block appended (separated by a blank line). Migration for pre-Phase-P consumer repos: `project-guide init --force` rewrites the block; `git rm --cached` is the manual cleanup for already-tracked files.
294
+ **Migration:** `project-guide init --force` rewrites prior blocks (pre-P.d `.bak.*`-only, v2.6.0 4-line, v2.6.1/v2.7.0 3-line) to the v2.7.1 explicit-list form in place. `git rm --cached` remains the manual cleanup for files already tracked under the old policy.
285
295
 
286
296
  ---
287
297
 
@@ -312,39 +312,62 @@ _GITIGNORE_HEADER = "# project-guide"
312
312
  def _build_project_guide_block(target_dir: str) -> str:
313
313
  """Build the canonical project-guide gitignore block.
314
314
 
315
- Policy (Story P.d, tightened in P.j): everything under ``target_dir`` is
316
- gitignored except ``go.md`` (which the LLM reads, and IDE-integrated
317
- LLMs typically hide gitignored files from the LLM's view). The remaining
318
- template tree including ``.bak.*`` backup files — is bundled static
319
- data that ``heal`` repopulates on first invocation, so it does not need
320
- to be tracked in the consumer repo. P.j dropped the explicit ``.bak.*``
321
- line because the broad ``**`` rule already covers it; the old line is
322
- still recognized as ours so v2.6.0 installs heal to this shape on
323
- ``init --force``.
315
+ Policy (Story P.d, tightened in P.j, reshaped in P.l): everything under
316
+ ``target_dir`` is gitignored except ``go.md``. ``go.md`` must remain
317
+ tracked because IDE-integrated LLMs (Cursor, parts of the VS Code fork
318
+ ecosystem, several LSP-based search backends) typically hide gitignored
319
+ files from the LLM's @-mention / fuzzy-search view.
320
+
321
+ P.l (v2.7.1) abandons the cleaner ``<target>/**`` + ``!<target>/go.md``
322
+ shape because several of those same IDEs implement a subset of
323
+ ``.gitignore`` semantics that does not honor re-include negation —
324
+ they apply the broad ``**`` rule, hide ``go.md``, and defeat the
325
+ visibility constraint the policy is trying to enforce. The new form
326
+ lists every top-level entry under ``target_dir`` explicitly so no
327
+ negation is required. The list is enumerated from the bundled template
328
+ tree at write time, so future additions to the install footprint are
329
+ picked up automatically.
330
+
331
+ The trailing ``<target>/**/*.bak.*`` rule defensively ignores backup
332
+ files that ``apply_file_update`` writes next to top-level synced files
333
+ (subdirectory backups are already covered by the per-directory entries).
324
334
  """
325
- return (
326
- f"{_GITIGNORE_HEADER}\n"
327
- f"{target_dir}/**\n"
328
- f"!{target_dir}/go.md\n"
329
- )
330
-
331
-
332
- def _recognized_block_lines(target_dir: str) -> set[str]:
333
- """Lines that mark an existing block as one we wrote (safe to replace).
334
-
335
- Any block whose every non-empty line is in this set can be safely
336
- rewritten to the canonical form. A block containing anything outside
337
- this set has been hand-customized; we warn and leave it alone.
335
+ pkg_root = _get_package_template_dir()
336
+ entries: list[str] = [_GITIGNORE_HEADER]
337
+ for child in sorted(pkg_root.iterdir(), key=lambda p: p.name):
338
+ if child.name == "go.md":
339
+ continue
340
+ suffix = "/" if child.is_dir() else ""
341
+ entries.append(f"/{target_dir}/{child.name}{suffix}")
342
+ entries.append(f"/{target_dir}/**/*.bak.*")
343
+ return "\n".join(entries) + "\n"
344
+
345
+
346
+ def _is_recognized_block_line(line: str, target_dir: str) -> bool:
347
+ """Return True when ``line`` is one we plausibly wrote in any past version.
348
+
349
+ A block whose every non-empty line satisfies this predicate is treated
350
+ as ours and rewritten cleanly to the current canonical form; a block
351
+ containing anything that fails this predicate is left untouched with a
352
+ warning. Recognized forms (newest first):
353
+
354
+ - **v2.7.1+ explicit-list form (Story P.l):** any line starting with
355
+ ``/<target>/``. The leading slash anchors at repo root; we never
356
+ write unanchored lines, and there is nothing else we plausibly
357
+ generate under that anchor.
358
+ - **v2.6.1 form (Story P.j):** ``<target>/**`` and ``!<target>/go.md``.
359
+ - **v2.6.0 form (Story P.d):** the v2.6.1 lines plus ``<target>/**/*.bak.*``.
360
+ - **pre-P.d form:** ``<target>/**/*.bak.*`` only.
361
+ - **Legacy variants:** ``<target>/go.md`` (incorrectly gitignored, if
362
+ it ever appeared in the wild).
338
363
  """
339
- return {
340
- # Canonical lines as of P.j (v2.6.1).
364
+ if line.startswith(f"/{target_dir}/"):
365
+ return True
366
+ return line in {
341
367
  f"{target_dir}/**",
342
368
  f"!{target_dir}/go.md",
343
- # v2.6.0 canonical form (Story P.d) — kept recognized so v2.6.0
344
- # installs heal to the P.j 3-line form on `init --force`.
345
369
  f"{target_dir}/**/*.bak.*",
346
- # Legacy / pre-P.d variants we know about.
347
- f"{target_dir}/go.md", # incorrectly gitignored go.md, if it ever appeared
370
+ f"{target_dir}/go.md",
348
371
  }
349
372
 
350
373
 
@@ -388,8 +411,7 @@ def _ensure_gitignore_entry(target_dir: str) -> None:
388
411
  block_end += 1
389
412
 
390
413
  block_body = [lines[i].strip() for i in range(header_idx + 1, block_end)]
391
- recognized = _recognized_block_lines(target_dir)
392
- foreign = [bl for bl in block_body if bl and bl not in recognized]
414
+ foreign = [bl for bl in block_body if bl and not _is_recognized_block_line(bl, target_dir)]
393
415
 
394
416
  if foreign:
395
417
  click.secho(
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = "2.7.0"
15
+ __version__ = "2.7.1"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "project-guide"
3
- version = "2.7.0"
3
+ version = "2.7.1"
4
4
  description = "Stay organized and in control with adaptive LLM workflow prompts."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -2404,24 +2404,41 @@ def test_hook_under_skip_input_heals_silently_with_notice(runner, tmp_path, hook
2404
2404
  # --- End Story P.c -----------------------------------------------------------
2405
2405
 
2406
2406
 
2407
- # --- Story P.d / P.j: gitignore block inversion + tightening ----------------
2407
+ # --- Story P.d / P.j / P.l: gitignore block inversion + tightening + IDE compat ---
2408
2408
 
2409
2409
 
2410
- _EXPECTED_GITIGNORE_BLOCK = (
2411
- "# project-guide\n"
2412
- "docs/project-guide/**\n"
2413
- "!docs/project-guide/go.md\n"
2414
- )
2410
+ def _expected_gitignore_block() -> str:
2411
+ """Compute the canonical gitignore block from the bundled template tree.
2412
+
2413
+ Mirrors `_build_project_guide_block()`'s enumeration so tests stay
2414
+ honest when the bundled tree changes — adding a new top-level template
2415
+ file/directory will be picked up by both the writer and this helper.
2416
+ """
2417
+ from project_guide.cli import _get_package_template_dir
2418
+
2419
+ pkg_root = _get_package_template_dir()
2420
+ target = "docs/project-guide"
2421
+ entries = ["# project-guide"]
2422
+ for child in sorted(pkg_root.iterdir(), key=lambda p: p.name):
2423
+ if child.name == "go.md":
2424
+ continue
2425
+ suffix = "/" if child.is_dir() else ""
2426
+ entries.append(f"/{target}/{child.name}{suffix}")
2427
+ entries.append(f"/{target}/**/*.bak.*")
2428
+ return "\n".join(entries) + "\n"
2415
2429
 
2416
2430
 
2417
2431
  def test_init_fresh_writes_inverted_gitignore_block(runner, tmp_path):
2418
- """Fresh `init` writes the canonical 3-line track-only-go.md block."""
2432
+ """Fresh `init` writes the canonical explicit-list track-only-go.md block."""
2419
2433
  with runner.isolated_filesystem(temp_dir=tmp_path):
2420
2434
  result = runner.invoke(main, ['init'])
2421
2435
 
2422
2436
  assert result.exit_code == 0, result.output
2423
2437
  gitignore = Path(".gitignore").read_text()
2424
- assert _EXPECTED_GITIGNORE_BLOCK in gitignore
2438
+ expected = _expected_gitignore_block()
2439
+ assert expected in gitignore
2440
+ # No negation pattern — the whole point of the P.l rewrite.
2441
+ assert "!" not in gitignore.split("# project-guide", 1)[1].split("\n\n", 1)[0]
2425
2442
 
2426
2443
 
2427
2444
  def test_init_force_rewrites_legacy_bak_only_block_cleanly(runner, tmp_path):
@@ -2442,17 +2459,15 @@ def test_init_force_rewrites_legacy_bak_only_block_cleanly(runner, tmp_path):
2442
2459
  gitignore = Path(".gitignore").read_text()
2443
2460
  # Old block lines that are not in the canonical form must be gone.
2444
2461
  assert gitignore.count("# project-guide") == 1, gitignore
2445
- assert _EXPECTED_GITIGNORE_BLOCK in gitignore
2446
- # The dropped legacy line must not survive.
2447
- assert "docs/project-guide/**/*.bak.*" not in gitignore
2462
+ assert _expected_gitignore_block() in gitignore
2448
2463
  # Surrounding content preserved.
2449
2464
  assert "*.pyc\n" in gitignore
2450
2465
 
2451
2466
 
2452
- def test_init_force_rewrites_v260_four_line_block_to_three_lines(runner, tmp_path):
2453
- """`init --force` on a v2.6.0-shipped 4-line block tightens it to the v2.6.1 3-line form (Story P.j)."""
2467
+ def test_init_force_rewrites_v260_four_line_block_to_explicit_list(runner, tmp_path):
2468
+ """`init --force` on a v2.6.0-shipped 4-line block migrates to the v2.7.1 explicit-list form."""
2454
2469
  with runner.isolated_filesystem(temp_dir=tmp_path):
2455
- # v2.6.0 canonical form — the line P.j drops is still recognized,
2470
+ # v2.6.0 canonical form — all four lines are still recognized,
2456
2471
  # so the writer cleanly rewrites the block.
2457
2472
  Path(".gitignore").write_text(
2458
2473
  "*.pyc\n"
@@ -2468,16 +2483,36 @@ def test_init_force_rewrites_v260_four_line_block_to_three_lines(runner, tmp_pat
2468
2483
  assert result.exit_code == 0, result.output
2469
2484
  gitignore = Path(".gitignore").read_text()
2470
2485
  assert gitignore.count("# project-guide") == 1, gitignore
2471
- assert _EXPECTED_GITIGNORE_BLOCK in gitignore
2472
- # The redundant v2.6.0 line must be gone.
2473
- assert "docs/project-guide/**/*.bak.*" not in gitignore
2486
+ assert _expected_gitignore_block() in gitignore
2487
+ # The old negation form must be gone.
2488
+ assert "!docs/project-guide/go.md" not in gitignore
2474
2489
  assert "*.pyc\n" in gitignore
2475
2490
 
2476
2491
 
2492
+ def test_init_force_rewrites_v261_three_line_block_to_explicit_list(runner, tmp_path):
2493
+ """`init --force` on a v2.6.1/v2.7.0-shipped 3-line negation block migrates to v2.7.1 explicit-list."""
2494
+ with runner.isolated_filesystem(temp_dir=tmp_path):
2495
+ Path(".gitignore").write_text(
2496
+ "*.pyc\n"
2497
+ "\n"
2498
+ "# project-guide\n"
2499
+ "docs/project-guide/**\n"
2500
+ "!docs/project-guide/go.md\n"
2501
+ )
2502
+
2503
+ result = runner.invoke(main, ['init', '--force'])
2504
+
2505
+ assert result.exit_code == 0, result.output
2506
+ gitignore = Path(".gitignore").read_text()
2507
+ assert gitignore.count("# project-guide") == 1, gitignore
2508
+ assert _expected_gitignore_block() in gitignore
2509
+ assert "!docs/project-guide/go.md" not in gitignore
2510
+
2511
+
2477
2512
  def test_init_with_existing_canonical_block_is_idempotent(runner, tmp_path):
2478
2513
  """Running `init` over a project whose .gitignore is already canonical does not rewrite."""
2479
2514
  with runner.isolated_filesystem(temp_dir=tmp_path):
2480
- Path(".gitignore").write_text("foo\n\n" + _EXPECTED_GITIGNORE_BLOCK)
2515
+ Path(".gitignore").write_text("foo\n\n" + _expected_gitignore_block())
2481
2516
  before = Path(".gitignore").read_text()
2482
2517
 
2483
2518
  result = runner.invoke(main, ['init'])
@@ -2493,7 +2528,7 @@ def test_init_warns_on_foreign_project_guide_block_and_leaves_it_untouched(runne
2493
2528
  foreign_block = (
2494
2529
  "# project-guide\n"
2495
2530
  "docs/project-guide/**/*.bak.*\n"
2496
- "docs/project-guide/local-only.md\n" # foreign line
2531
+ "some/unrelated/path\n" # foreign line — not under /docs/project-guide/
2497
2532
  )
2498
2533
  Path(".gitignore").write_text(foreign_block)
2499
2534
 
@@ -2516,10 +2551,10 @@ def test_init_appends_block_when_no_prior_project_guide_section(runner, tmp_path
2516
2551
  gitignore = Path(".gitignore").read_text()
2517
2552
  assert "*.pyc\n" in gitignore
2518
2553
  assert ".venv/\n" in gitignore
2519
- assert _EXPECTED_GITIGNORE_BLOCK in gitignore
2554
+ assert _expected_gitignore_block() in gitignore
2520
2555
 
2521
2556
 
2522
- # --- End Story P.d / P.j ----------------------------------------------------
2557
+ # --- End Story P.d / P.j / P.l ----------------------------------------------
2523
2558
 
2524
2559
 
2525
2560
  # --- Story P.k: project-guide git-push wrapper ------------------------------
File without changes
File without changes
File without changes
File without changes
File without changes