project-guide 2.7.0__tar.gz → 2.7.2__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.
- {project_guide-2.7.0 → project_guide-2.7.2}/.gitignore +5 -3
- {project_guide-2.7.0 → project_guide-2.7.2}/.project-guide.yml +2 -2
- {project_guide-2.7.0 → project_guide-2.7.2}/CHANGELOG.md +38 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/PKG-INFO +1 -1
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/features.md +9 -1
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/project-essentials.md +9 -3
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/stories.md +81 -1
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/tech-spec.md +16 -6
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/actions.py +4 -2
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/cli.py +55 -32
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/stories.py +4 -1
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/_phase-letters.md +9 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/version.py +1 -1
- {project_guide-2.7.0 → project_guide-2.7.2}/pyproject.toml +1 -1
- {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_actions.py +13 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_cli.py +139 -21
- project_guide-2.7.2/tests/test_stories.py +141 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/.github/FUNDING.yml +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/.github/dependabot.yml +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/.github/workflows/ci.yml +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/.github/workflows/deploy-docs.yml +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/.github/workflows/publish.yml +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/.github/workflows/test.yml +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/.pyve/config +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/.tool-versions +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/CONTRIBUTING.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/LICENSE +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/README.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/SECURITY.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/.gitignore +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/about/changelog.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/about/license.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/developer-guide/contributing.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/developer-guide/development.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/developer-guide/testing.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/getting-started.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/images/project-guide-banner-landing.png +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/images/project-guide-header-readme.png +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/index.html +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/user-guide/commands.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/user-guide/configuration.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/user-guide/install-options.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/user-guide/modes.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/user-guide/overrides.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/site/user-guide/workflow.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/phase-j-modes-plan.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/phase-k-release-lifecycle-plan.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/phase-l-no-input-init-plan.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/phase-m-project-essentials-plan.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/phase-n-mode-naming-cli-memory-plan.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/phase-o-pyve-quiet-embedding-subplan.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/phase-o-quiet-non-interactive-embedding-feature.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/project-guide-no-input-spec.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/stories-v1.3.1.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/stories-v2.0.20.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/stories-v2.3.9.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/stories-v2.4.19.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/stories-v2.5.15.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/.archive/ux-problems-v2.0.10.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/brand-descriptions.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/concept.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/docs/specs/phase-p-auto-heal-plan.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/mkdocs.yml +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project-guide-old-template.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/__init__.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/__main__.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/config.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/exceptions.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/metadata.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/render.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/runtime.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/sync.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/.project-guide.yml.template +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/.metadata.yml +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/README.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/best-practices-guide.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/brand-descriptions-guide.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/codecov-setup-guide.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/debug-guide.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/landing-page-guide.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/production-github-guide.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/project-guide.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/python-editable-install.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/brand-descriptions.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/concept.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/features.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/project-essentials.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/pyve-essentials.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/stories.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/tech-spec.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/llm_entry_point.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/_header-common.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/_header-cycle.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/_header-sequence.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/archive-stories-mode.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/code-direct-mode.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/code-test-first-mode.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/debug-mode.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/default-mode.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/document-brand-mode.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/document-landing-mode.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-concept-mode.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-features-mode.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-phase-mode.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-production-phase-mode.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-stories-mode.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-tech-spec-mode.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/refactor-document-mode.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/refactor-plan-mode.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/scaffold-project-mode.md +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/requirements-dev.txt +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/tests/__init__.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/tests/conftest.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_archive_stories_mode.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_config.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_integration.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_metadata.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_purge.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_render.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_runtime.py +0 -0
- {project_guide-2.7.0 → project_guide-2.7.2}/tests/test_sync.py +0 -0
|
@@ -32,6 +32,8 @@ build/
|
|
|
32
32
|
/site/
|
|
33
33
|
|
|
34
34
|
# project-guide
|
|
35
|
-
docs/project-guide
|
|
36
|
-
|
|
37
|
-
docs/project-guide
|
|
35
|
+
/docs/project-guide/.metadata.yml
|
|
36
|
+
/docs/project-guide/README.md
|
|
37
|
+
/docs/project-guide/developer/
|
|
38
|
+
/docs/project-guide/templates/
|
|
39
|
+
/docs/project-guide/**/*.bak.*
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
version: '2.0'
|
|
2
|
-
installed_version: 2.
|
|
2
|
+
installed_version: 2.7.2
|
|
3
3
|
target_dir: docs/project-guide
|
|
4
4
|
metadata_file: .metadata.yml
|
|
5
|
-
current_mode:
|
|
5
|
+
current_mode: debug
|
|
6
6
|
test_first: false
|
|
7
7
|
pyve_version: pyve version 2.6.2
|
|
8
8
|
project_name: project-guide
|
|
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.7.2] - 2026-05-19
|
|
11
|
+
|
|
12
|
+
Bug fix: `project-guide git-push` (and version/phase detection) silently dropped sub-numbered story IDs (`J.m.1`, `J.m.2`, …). When the latest `[Done]` story used the sub-numbered form, the wrapper fell back to the previous bare-letter heading and reported it as "already committed," blocking the push of the new story.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **`project_guide/stories.py:_STORY_RE`** — extended the story-ID character class from `[A-Z]\.[a-z]+` to `[A-Z]\.[a-z]+(?:\.\d+)?` so headings of the form `### Story J.m.1: ...` parse correctly. Without this, `_read_done_stories()` silently filtered such headings out and the wrapper picked the wrong "last [Done] story."
|
|
16
|
+
- **`project_guide/cli.py:_COMMIT_SUBJECT_STORY_ID_RE`** — extended in the same shape so the already-committed check recognizes commit subjects like `J.m.1: ...`. Without this fix the duplicate-detection path would have a second-layer hole even after the stories.md side was fixed.
|
|
17
|
+
- **`project_guide/actions.py:_VERSION_RE`** — extended in the same shape so `detect_latest_version()` no longer under-reports the highest version when the latest story uses the sub-numbered form.
|
|
18
|
+
|
|
19
|
+
### Documented
|
|
20
|
+
- **`_phase-letters.md`** — added a "Sub-numbered stories" subsection covering both use cases: pre-implementation split (`J.m` was planned but split into `J.m.1`, `J.m.2` before any work; bare `J.m` heading dropped) and post-implementation follow-up (`J.m` shipped, then a bug or follow-on feature lands as `J.m.1` before proceeding to `J.n`). Flat single-level only — no cascading like `J.m.1.1`.
|
|
21
|
+
|
|
22
|
+
### Tests
|
|
23
|
+
- New `tests/test_stories.py` covering `_STORY_RE`, `_read_done_stories()`, and `derive_commit_message()` — `stories.py` had no direct unit tests prior to this story (the test-coverage gap that let the bug ship in P.k).
|
|
24
|
+
- New `tests/test_cli.py` cases for both `git-push` scenarios (post-impl follow-up with bare `J.m` present; pre-impl split with no bare `J.m`) and the round-trip "sub-numbered story is already committed" path.
|
|
25
|
+
- New `tests/test_actions.py` case asserting `detect_latest_version()` picks up the version on a sub-numbered heading.
|
|
26
|
+
|
|
27
|
+
## [2.7.1] - 2026-05-11
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- **`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:
|
|
33
|
+
```
|
|
34
|
+
# project-guide
|
|
35
|
+
/docs/project-guide/.metadata.yml
|
|
36
|
+
/docs/project-guide/README.md
|
|
37
|
+
/docs/project-guide/developer/
|
|
38
|
+
/docs/project-guide/templates/
|
|
39
|
+
/docs/project-guide/**/*.bak.*
|
|
40
|
+
```
|
|
41
|
+
- **`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.
|
|
42
|
+
- **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.
|
|
43
|
+
- **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.
|
|
44
|
+
|
|
45
|
+
### Migration
|
|
46
|
+
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.
|
|
47
|
+
|
|
10
48
|
## [2.7.0] - 2026-05-11
|
|
11
49
|
|
|
12
50
|
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.
|
|
3
|
+
Version: 2.7.2
|
|
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
|
|
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
|
|
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
|
-
**
|
|
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.
|
|
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,86 @@ 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
|
+
|
|
311
|
+
### Story P.m: v2.7.2 Recognize sub-numbered story IDs (`J.m.1`) in regex sites [Done]
|
|
312
|
+
|
|
313
|
+
Reported bug: in a consumer project with the heading sequence `… J.l [Done], J.m.1 [Done]`, `project-guide git-push` printed `"Story J.l is already committed. Use 'git-push' directly for any follow-up commit."` and refused to push, even though `J.m.1` was the actual just-completed story.
|
|
314
|
+
|
|
315
|
+
**Root cause:** three regexes encoded the story-ID shape as `[A-Z]\.[a-z]+`, which silently fails to match the sub-numbered form. `stories.py:_STORY_RE` was the proximate cause — `_read_done_stories()` filtered the `J.m.1` heading out entirely, leaving `J.l` as the "last `[Done]`," and the commit-subject check then correctly observed that `J.l` had been committed. The other two sites (`cli.py:_COMMIT_SUBJECT_STORY_ID_RE`, `actions.py:_VERSION_RE`) had the same hole and were fixed in the same story to avoid a half-fix where a future code path re-introduced the bug from a different angle.
|
|
316
|
+
|
|
317
|
+
**Sub-numbered form** — `<phase>.<letter>.<digit>+`, flat single-level only (no cascading like `J.m.1.1`). Two use cases per the developer's intent:
|
|
318
|
+
|
|
319
|
+
- **Pre-implementation split:** `J.m` is planned but its scope is judged too large before any work begins; the heading is split into `J.m.1`, `J.m.2` and the bare `J.m` heading is dropped. Sequence: `… J.l, J.m.1, J.m.2, J.n, …`.
|
|
320
|
+
- **Post-implementation follow-up:** `J.m` ships, then a bug or follow-on feature must land before proceeding to `J.n`; the follow-up is added as `J.m.1` (and may cascade to `J.m.2`, `J.m.3`, …). Sequence: `… J.l, J.m, J.m.1, J.m.2, …, J.n, …`.
|
|
321
|
+
|
|
322
|
+
Both scenarios are exercised by the new test suite.
|
|
323
|
+
|
|
324
|
+
- [x] `project_guide/stories.py:_STORY_RE` — extend capture group to `[A-Z]\.[a-z]+(?:\.\d+)?` with comment cross-referencing `_phase-letters.md`
|
|
325
|
+
- [x] `project_guide/cli.py:_COMMIT_SUBJECT_STORY_ID_RE` — matching extension; updated comment to reflect the new shape so future readers see why the two regexes must move together
|
|
326
|
+
- [x] `project_guide/actions.py:_VERSION_RE` — matching extension; `detect_latest_version()` now picks up versions on sub-numbered headings
|
|
327
|
+
- [x] `project_guide/templates/project-guide/templates/modes/_phase-letters.md` — new "Sub-numbered stories" subsection documenting both scenarios and the flat-only constraint
|
|
328
|
+
- [x] Ran `project-guide update` to propagate the template change into `docs/project-guide/templates/modes/_phase-letters.md` (installed copy)
|
|
329
|
+
- [x] New `tests/test_stories.py` — first direct unit-test coverage for `stories.py` (the gap that let the bug ship in P.k). Covers `_STORY_RE` matches (plain, sub-numbered, multi-digit sub-number), `_read_done_stories()` for both scenarios, `derive_commit_message()` with sub-numbered ID, and the round-trip through `cli._COMMIT_SUBJECT_STORY_ID_RE`
|
|
330
|
+
- [x] `tests/test_cli.py` — new cases for both `git-push` scenarios (post-impl follow-up; pre-impl split with no bare `J.m`) and the "sub-numbered story is already committed" path that surfaces the second-layer regex hole
|
|
331
|
+
- [x] `tests/test_actions.py` — new case asserting `detect_latest_version()` picks up the version on a sub-numbered heading
|
|
332
|
+
- [x] Bump `project_guide/version.py`: `__version__ = "2.7.2"`
|
|
333
|
+
- [x] Bump `pyproject.toml`: `version = "2.7.2"`
|
|
334
|
+
- [x] Add `## [2.7.2] - 2026-05-19` entry to `CHANGELOG.md` framed as a bug fix with test-coverage backfill
|
|
335
|
+
|
|
336
|
+
**Migration:** none. The change is purely permissive — existing plain-letter IDs (`J.l`, `J.m`) continue to match. Consumers who never used sub-numbered IDs see no behavior change; consumers who did will see `git-push` and `detect_latest_version()` start handling those headings correctly.
|
|
337
|
+
|
|
338
|
+
**Why the docs change rides this story:** the sub-numbered form was already in use by consumers but undocumented in `_phase-letters.md`. Extending the regex without documenting the form would leave the next maintainer staring at an unexplained `(?:\.\d+)?` tail. Doc + code travel together.
|
|
339
|
+
|
|
340
|
+
**Out of scope:**
|
|
341
|
+
- Multi-level cascading (`J.m.1.1`). The developer's intent is flat; YAGNI until a consumer asks. Adding it later is a strict superset (`(?:\.\d+)+` instead of `(?:\.\d+)?`) and would be backwards compatible.
|
|
342
|
+
- A `project-guide check` rule that warns when a sub-numbered ID appears without a preceding plain-letter heading in non-pre-split contexts. The two valid scenarios cover everything seen in the wild; defer until there's evidence of misuse.
|
|
343
|
+
- Sub-numbered phase letters (`AA.1`). Phases don't carry the same pre-impl-split / post-impl-followup workflow that motivates the story-level sub-numbering, and no consumer has asked.
|
|
344
|
+
|
|
265
345
|
---
|
|
266
346
|
|
|
267
347
|
## 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` (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
**
|
|
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
|
|
|
@@ -127,9 +127,11 @@ class Artifact:
|
|
|
127
127
|
action=action,
|
|
128
128
|
)
|
|
129
129
|
|
|
130
|
-
# Matches `### Story <Phase>.<sub>: vMAJOR.MINOR.PATCH ...`
|
|
130
|
+
# Matches `### Story <Phase>.<sub>: vMAJOR.MINOR.PATCH ...` — the optional
|
|
131
|
+
# `(?:\.\d+)?` tail covers sub-numbered IDs (`J.m.1`, `J.m.2`, …) per the
|
|
132
|
+
# `_phase-letters.md` "Sub-numbered stories" rule.
|
|
131
133
|
_VERSION_RE = re.compile(
|
|
132
|
-
r"^###\s+Story\s+[A-Za-z]+\.[a-z]
|
|
134
|
+
r"^###\s+Story\s+[A-Za-z]+\.[a-z]+(?:\.\d+)?:\s+v(\d+)\.(\d+)\.(\d+)",
|
|
133
135
|
re.M,
|
|
134
136
|
)
|
|
135
137
|
|
|
@@ -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
|
|
316
|
-
gitignored except ``go.md``
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
``
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
""
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
340
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
@@ -1604,8 +1626,9 @@ def heal(no_input: bool):
|
|
|
1604
1626
|
|
|
1605
1627
|
# Regex matching the story-ID prefix of a commit-subject line — used to detect
|
|
1606
1628
|
# which stories have already been committed (Story P.k). Mirrors the
|
|
1607
|
-
# `[A-Z]\.[a-z]
|
|
1608
|
-
|
|
1629
|
+
# `[A-Z]\.[a-z]+(?:\.\d+)?` shape of `_STORY_RE` in `stories.py` (the optional
|
|
1630
|
+
# `.\d+` tail covers sub-numbered IDs like `J.m.1`).
|
|
1631
|
+
_COMMIT_SUBJECT_STORY_ID_RE = __import__("re").compile(r"^([A-Z]\.[a-z]+(?:\.\d+)?):\s")
|
|
1609
1632
|
|
|
1610
1633
|
|
|
1611
1634
|
def _get_committed_story_ids() -> set[str]:
|
|
@@ -17,8 +17,11 @@ from dataclasses import dataclass, field
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
19
|
# Matches: ### Story N.a: v2.4.0 Some Title [Done]
|
|
20
|
+
# The optional `(?:\.\d+)?` tail captures sub-numbered IDs (`J.m.1`, `J.m.2`, …)
|
|
21
|
+
# used for pre-implementation splits or post-implementation follow-ups. See
|
|
22
|
+
# `_phase-letters.md` (Sub-numbered stories).
|
|
20
23
|
_STORY_RE = re.compile(
|
|
21
|
-
r"^### Story ([A-Z]\.[a-z]+): (.+) \[(Done|In Progress|Planned)\]\s*$",
|
|
24
|
+
r"^### Story ([A-Z]\.[a-z]+(?:\.\d+)?): (.+) \[(Done|In Progress|Planned)\]\s*$",
|
|
22
25
|
re.MULTILINE,
|
|
23
26
|
)
|
|
24
27
|
|
|
@@ -14,6 +14,15 @@ Within a phase, stories use lowercase letters following the same scheme: `A.a`,
|
|
|
14
14
|
|
|
15
15
|
Examples: `A.a`, `A.b`, …, `A.z`, `A.aa`, `A.ab`, ….
|
|
16
16
|
|
|
17
|
+
### Sub-numbered stories
|
|
18
|
+
|
|
19
|
+
A story may carry an optional numeric suffix — `J.m.1`, `J.m.2`, … — appended after the sub-letter. Sub-numbers are flat (no cascading like `J.m.1.1`) and start at `1`. Two situations use them:
|
|
20
|
+
|
|
21
|
+
- **Pre-implementation split.** When `J.m` is planned but its scope is judged too large before any work begins, the heading is split into `J.m.1`, `J.m.2`, … and the bare `J.m` heading is dropped. Sequence becomes `…, J.l, J.m.1, J.m.2, J.n, …`.
|
|
22
|
+
- **Post-implementation follow-up.** When `J.m` ships but a bug or follow-on feature must land before proceeding to `J.n`, the follow-up is added as `J.m.1` (and may cascade to `J.m.2`, `J.m.3`, …). Sequence becomes `…, J.l, J.m, J.m.1, J.m.2, …, J.n, …`.
|
|
23
|
+
|
|
24
|
+
Sub-numbered stories follow normal Version Cadence: each one that ships code takes its own bump.
|
|
25
|
+
|
|
17
26
|
### Continuing across archive boundaries
|
|
18
27
|
|
|
19
28
|
When `stories.md` is archived (via `archive_stories` mode), the fresh `stories.md` starts empty — but phase letters do **not** reset. To determine the next phase letter:
|
|
@@ -150,6 +150,19 @@ def test_detect_latest_version_raises_when_none_found():
|
|
|
150
150
|
detect_latest_version(text)
|
|
151
151
|
|
|
152
152
|
|
|
153
|
+
def test_detect_latest_version_recognizes_subnumbered_story_id():
|
|
154
|
+
"""Sub-numbered story IDs (J.m.1, used for pre-impl splits or post-impl
|
|
155
|
+
follow-ups) must contribute to the latest-version computation. The
|
|
156
|
+
plain-letter regex silently dropped them, causing `detect_latest_version`
|
|
157
|
+
to under-report the highest version when the latest story used the
|
|
158
|
+
`.NN` form."""
|
|
159
|
+
text = (
|
|
160
|
+
"### Story J.l: v0.68.0 prior story [Done]\n\n"
|
|
161
|
+
"### Story J.m.1: v0.70.0 follow-up after J.m [Done]\n"
|
|
162
|
+
)
|
|
163
|
+
assert detect_latest_version(text) == (0, 70, 0)
|
|
164
|
+
|
|
165
|
+
|
|
153
166
|
# ---------------------------------------------------------------------------
|
|
154
167
|
# detect_latest_phase_letter
|
|
155
168
|
# ---------------------------------------------------------------------------
|