project-guide 2.7.1__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.
Files changed (121) hide show
  1. {project_guide-2.7.1 → project_guide-2.7.2}/.gitignore +5 -3
  2. {project_guide-2.7.1 → project_guide-2.7.2}/.project-guide.yml +2 -2
  3. {project_guide-2.7.1 → project_guide-2.7.2}/CHANGELOG.md +17 -0
  4. {project_guide-2.7.1 → project_guide-2.7.2}/PKG-INFO +1 -1
  5. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/stories.md +34 -0
  6. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/actions.py +4 -2
  7. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/cli.py +3 -2
  8. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/stories.py +4 -1
  9. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/_phase-letters.md +9 -0
  10. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/version.py +1 -1
  11. {project_guide-2.7.1 → project_guide-2.7.2}/pyproject.toml +1 -1
  12. {project_guide-2.7.1 → project_guide-2.7.2}/tests/test_actions.py +13 -0
  13. {project_guide-2.7.1 → project_guide-2.7.2}/tests/test_cli.py +83 -0
  14. project_guide-2.7.2/tests/test_stories.py +141 -0
  15. {project_guide-2.7.1 → project_guide-2.7.2}/.github/FUNDING.yml +0 -0
  16. {project_guide-2.7.1 → project_guide-2.7.2}/.github/dependabot.yml +0 -0
  17. {project_guide-2.7.1 → project_guide-2.7.2}/.github/workflows/ci.yml +0 -0
  18. {project_guide-2.7.1 → project_guide-2.7.2}/.github/workflows/deploy-docs.yml +0 -0
  19. {project_guide-2.7.1 → project_guide-2.7.2}/.github/workflows/publish.yml +0 -0
  20. {project_guide-2.7.1 → project_guide-2.7.2}/.github/workflows/test.yml +0 -0
  21. {project_guide-2.7.1 → project_guide-2.7.2}/.pyve/config +0 -0
  22. {project_guide-2.7.1 → project_guide-2.7.2}/.tool-versions +0 -0
  23. {project_guide-2.7.1 → project_guide-2.7.2}/CONTRIBUTING.md +0 -0
  24. {project_guide-2.7.1 → project_guide-2.7.2}/LICENSE +0 -0
  25. {project_guide-2.7.1 → project_guide-2.7.2}/README.md +0 -0
  26. {project_guide-2.7.1 → project_guide-2.7.2}/SECURITY.md +0 -0
  27. {project_guide-2.7.1 → project_guide-2.7.2}/docs/site/.gitignore +0 -0
  28. {project_guide-2.7.1 → project_guide-2.7.2}/docs/site/about/changelog.md +0 -0
  29. {project_guide-2.7.1 → project_guide-2.7.2}/docs/site/about/license.md +0 -0
  30. {project_guide-2.7.1 → project_guide-2.7.2}/docs/site/developer-guide/contributing.md +0 -0
  31. {project_guide-2.7.1 → project_guide-2.7.2}/docs/site/developer-guide/development.md +0 -0
  32. {project_guide-2.7.1 → project_guide-2.7.2}/docs/site/developer-guide/testing.md +0 -0
  33. {project_guide-2.7.1 → project_guide-2.7.2}/docs/site/getting-started.md +0 -0
  34. {project_guide-2.7.1 → project_guide-2.7.2}/docs/site/images/project-guide-banner-landing.png +0 -0
  35. {project_guide-2.7.1 → project_guide-2.7.2}/docs/site/images/project-guide-header-readme.png +0 -0
  36. {project_guide-2.7.1 → project_guide-2.7.2}/docs/site/index.html +0 -0
  37. {project_guide-2.7.1 → project_guide-2.7.2}/docs/site/user-guide/commands.md +0 -0
  38. {project_guide-2.7.1 → project_guide-2.7.2}/docs/site/user-guide/configuration.md +0 -0
  39. {project_guide-2.7.1 → project_guide-2.7.2}/docs/site/user-guide/install-options.md +0 -0
  40. {project_guide-2.7.1 → project_guide-2.7.2}/docs/site/user-guide/modes.md +0 -0
  41. {project_guide-2.7.1 → project_guide-2.7.2}/docs/site/user-guide/overrides.md +0 -0
  42. {project_guide-2.7.1 → project_guide-2.7.2}/docs/site/user-guide/workflow.md +0 -0
  43. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/.archive/phase-j-modes-plan.md +0 -0
  44. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/.archive/phase-k-release-lifecycle-plan.md +0 -0
  45. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/.archive/phase-l-no-input-init-plan.md +0 -0
  46. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/.archive/phase-m-project-essentials-plan.md +0 -0
  47. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/.archive/phase-n-mode-naming-cli-memory-plan.md +0 -0
  48. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/.archive/phase-o-pyve-quiet-embedding-subplan.md +0 -0
  49. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/.archive/phase-o-quiet-non-interactive-embedding-feature.md +0 -0
  50. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/.archive/project-guide-no-input-spec.md +0 -0
  51. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/.archive/stories-v1.3.1.md +0 -0
  52. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/.archive/stories-v2.0.20.md +0 -0
  53. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/.archive/stories-v2.3.9.md +0 -0
  54. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/.archive/stories-v2.4.19.md +0 -0
  55. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/.archive/stories-v2.5.15.md +0 -0
  56. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/.archive/ux-problems-v2.0.10.md +0 -0
  57. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/brand-descriptions.md +0 -0
  58. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/concept.md +0 -0
  59. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/features.md +0 -0
  60. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/phase-p-auto-heal-plan.md +0 -0
  61. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/project-essentials.md +0 -0
  62. {project_guide-2.7.1 → project_guide-2.7.2}/docs/specs/tech-spec.md +0 -0
  63. {project_guide-2.7.1 → project_guide-2.7.2}/mkdocs.yml +0 -0
  64. {project_guide-2.7.1 → project_guide-2.7.2}/project-guide-old-template.md +0 -0
  65. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/__init__.py +0 -0
  66. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/__main__.py +0 -0
  67. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/config.py +0 -0
  68. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/exceptions.py +0 -0
  69. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/metadata.py +0 -0
  70. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/render.py +0 -0
  71. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/runtime.py +0 -0
  72. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/sync.py +0 -0
  73. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/.project-guide.yml.template +0 -0
  74. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/.metadata.yml +0 -0
  75. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/README.md +0 -0
  76. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/best-practices-guide.md +0 -0
  77. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/brand-descriptions-guide.md +0 -0
  78. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/codecov-setup-guide.md +0 -0
  79. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/debug-guide.md +0 -0
  80. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/landing-page-guide.md +0 -0
  81. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/production-github-guide.md +0 -0
  82. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/project-guide.md +0 -0
  83. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/developer/python-editable-install.md +0 -0
  84. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/brand-descriptions.md +0 -0
  85. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/concept.md +0 -0
  86. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/features.md +0 -0
  87. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/project-essentials.md +0 -0
  88. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/pyve-essentials.md +0 -0
  89. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/stories.md +0 -0
  90. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/artifacts/tech-spec.md +0 -0
  91. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/llm_entry_point.md +0 -0
  92. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/_header-common.md +0 -0
  93. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/_header-cycle.md +0 -0
  94. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/_header-sequence.md +0 -0
  95. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/archive-stories-mode.md +0 -0
  96. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/code-direct-mode.md +0 -0
  97. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/code-test-first-mode.md +0 -0
  98. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/debug-mode.md +0 -0
  99. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/default-mode.md +0 -0
  100. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/document-brand-mode.md +0 -0
  101. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/document-landing-mode.md +0 -0
  102. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-concept-mode.md +0 -0
  103. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-features-mode.md +0 -0
  104. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-phase-mode.md +0 -0
  105. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-production-phase-mode.md +0 -0
  106. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-stories-mode.md +0 -0
  107. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/plan-tech-spec-mode.md +0 -0
  108. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/refactor-document-mode.md +0 -0
  109. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/refactor-plan-mode.md +0 -0
  110. {project_guide-2.7.1 → project_guide-2.7.2}/project_guide/templates/project-guide/templates/modes/scaffold-project-mode.md +0 -0
  111. {project_guide-2.7.1 → project_guide-2.7.2}/requirements-dev.txt +0 -0
  112. {project_guide-2.7.1 → project_guide-2.7.2}/tests/__init__.py +0 -0
  113. {project_guide-2.7.1 → project_guide-2.7.2}/tests/conftest.py +0 -0
  114. {project_guide-2.7.1 → project_guide-2.7.2}/tests/test_archive_stories_mode.py +0 -0
  115. {project_guide-2.7.1 → project_guide-2.7.2}/tests/test_config.py +0 -0
  116. {project_guide-2.7.1 → project_guide-2.7.2}/tests/test_integration.py +0 -0
  117. {project_guide-2.7.1 → project_guide-2.7.2}/tests/test_metadata.py +0 -0
  118. {project_guide-2.7.1 → project_guide-2.7.2}/tests/test_purge.py +0 -0
  119. {project_guide-2.7.1 → project_guide-2.7.2}/tests/test_render.py +0 -0
  120. {project_guide-2.7.1 → project_guide-2.7.2}/tests/test_runtime.py +0 -0
  121. {project_guide-2.7.1 → 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
- !docs/project-guide/go.md
37
- docs/project-guide/**/*.bak.*
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.6.0
2
+ installed_version: 2.7.2
3
3
  target_dir: docs/project-guide
4
4
  metadata_file: .metadata.yml
5
- current_mode: default
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,23 @@ 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
+
10
27
  ## [2.7.1] - 2026-05-11
11
28
 
12
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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: project-guide
3
- Version: 2.7.1
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/
@@ -308,6 +308,40 @@ The new canonical form lists every gitignored top-level entry explicitly so no n
308
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
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
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
+
311
345
  ---
312
346
 
313
347
  ## Future
@@ -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]+:\s+v(\d+)\.(\d+)\.(\d+)",
134
+ r"^###\s+Story\s+[A-Za-z]+\.[a-z]+(?:\.\d+)?:\s+v(\d+)\.(\d+)\.(\d+)",
133
135
  re.M,
134
136
  )
135
137
 
@@ -1626,8 +1626,9 @@ def heal(no_input: bool):
1626
1626
 
1627
1627
  # Regex matching the story-ID prefix of a commit-subject line — used to detect
1628
1628
  # which stories have already been committed (Story P.k). Mirrors the
1629
- # `[A-Z]\.[a-z]+` shape of `_STORY_RE` in `stories.py`.
1630
- _COMMIT_SUBJECT_STORY_ID_RE = __import__("re").compile(r"^([A-Z]\.[a-z]+):\s")
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")
1631
1632
 
1632
1633
 
1633
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:
@@ -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.1"
15
+ __version__ = "2.7.2"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "project-guide"
3
- version = "2.7.1"
3
+ version = "2.7.2"
4
4
  description = "Stay organized and in control with adaptive LLM workflow prompts."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -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
  # ---------------------------------------------------------------------------
@@ -2811,4 +2811,87 @@ def test_git_push_propagates_child_exit_code(runner, tmp_path, monkeypatch):
2811
2811
  assert result.exit_code == 7, result.output
2812
2812
 
2813
2813
 
2814
+ def test_git_push_picks_up_subnumbered_last_done_story_scenario_a(runner, tmp_path, monkeypatch):
2815
+ """Scenario A — post-implementation follow-up: ..., J.l, J.m, J.m.1.
2816
+
2817
+ Reported bug: when the last [Done] story is `J.m.1`, the wrapper
2818
+ silently fell back to `J.l` and reported it as already committed.
2819
+ """
2820
+ with runner.isolated_filesystem(temp_dir=tmp_path):
2821
+ _write_stories_md(
2822
+ "### Story J.l: update remaining refs [Done]",
2823
+ "### Story J.m: v0.69.0 Integrate component [Done]",
2824
+ "### Story J.m.1: v0.70.0 Follow-up after J.m [Done]",
2825
+ )
2826
+ _mock_git_push_on_path(monkeypatch)
2827
+ captured: list = []
2828
+ _mock_git_log_subjects(
2829
+ monkeypatch,
2830
+ subjects=[
2831
+ "J.l: update remaining refs",
2832
+ "J.m: v0.69.0 Integrate component",
2833
+ ],
2834
+ git_push_argv_capture=captured,
2835
+ )
2836
+
2837
+ result = runner.invoke(main, ['git-push'])
2838
+
2839
+ assert result.exit_code == 0, result.output
2840
+ assert len(captured) == 1, captured
2841
+ assert captured[0][1] == "J.m.1: v0.70.0 Follow-up after J.m"
2842
+
2843
+
2844
+ def test_git_push_picks_up_subnumbered_last_done_story_scenario_b(runner, tmp_path, monkeypatch):
2845
+ """Scenario B — pre-implementation split: ..., J.l, J.m.1, J.m.2 (no bare J.m).
2846
+
2847
+ When J.m's scope is split before implementation, the bare J.m heading
2848
+ never exists. The wrapper's "last [Done]" must be J.m.2.
2849
+ """
2850
+ with runner.isolated_filesystem(temp_dir=tmp_path):
2851
+ _write_stories_md(
2852
+ "### Story J.l: update remaining refs [Done]",
2853
+ "### Story J.m.1: v0.69.0 First half of the split [Done]",
2854
+ "### Story J.m.2: v0.70.0 Second half of the split [Done]",
2855
+ )
2856
+ _mock_git_push_on_path(monkeypatch)
2857
+ captured: list = []
2858
+ _mock_git_log_subjects(
2859
+ monkeypatch,
2860
+ subjects=[
2861
+ "J.l: update remaining refs",
2862
+ "J.m.1: v0.69.0 First half of the split",
2863
+ ],
2864
+ git_push_argv_capture=captured,
2865
+ )
2866
+
2867
+ result = runner.invoke(main, ['git-push'])
2868
+
2869
+ assert result.exit_code == 0, result.output
2870
+ assert len(captured) == 1, captured
2871
+ assert captured[0][1] == "J.m.2: v0.70.0 Second half of the split"
2872
+
2873
+
2874
+ def test_git_push_errors_when_subnumbered_story_is_already_committed(runner, tmp_path, monkeypatch):
2875
+ """Already-committed detection must recognize sub-numbered IDs in
2876
+ commit subjects (e.g. `J.m.1: ...`), not just `J.m: ...`. Without
2877
+ this, a sub-numbered story would be re-pushed silently."""
2878
+ with runner.isolated_filesystem(temp_dir=tmp_path):
2879
+ _write_stories_md(
2880
+ "### Story J.m.1: v0.70.0 Follow-up after J.m [Done]",
2881
+ )
2882
+ _mock_git_push_on_path(monkeypatch)
2883
+ captured: list = []
2884
+ _mock_git_log_subjects(
2885
+ monkeypatch,
2886
+ subjects=["J.m.1: v0.70.0 Follow-up after J.m"],
2887
+ git_push_argv_capture=captured,
2888
+ )
2889
+
2890
+ result = runner.invoke(main, ['git-push'])
2891
+
2892
+ assert result.exit_code == 1
2893
+ assert "Story J.m.1 is already committed" in result.output
2894
+ assert captured == [], "git-push must not be invoked when the story is already committed"
2895
+
2896
+
2814
2897
  # --- End Story P.k ----------------------------------------------------------
@@ -0,0 +1,141 @@
1
+ # Copyright (c) 2026 Pointmatic
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from pathlib import Path
16
+
17
+ from project_guide.stories import (
18
+ _STORY_RE,
19
+ StoryHeading,
20
+ _read_done_stories,
21
+ derive_commit_message,
22
+ )
23
+
24
+ _HEADER = """\
25
+ # stories.md -- testproject (python)
26
+
27
+ ## Phase J: Demo
28
+
29
+ """
30
+
31
+
32
+ def _write(tmp_path: Path, body: str) -> Path:
33
+ specs = tmp_path / "docs" / "specs"
34
+ specs.mkdir(parents=True)
35
+ (specs / "stories.md").write_text(_HEADER + body, encoding="utf-8")
36
+ return specs
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # _STORY_RE — direct regex coverage for sub-numbered story IDs
41
+ # ---------------------------------------------------------------------------
42
+
43
+
44
+ def test_story_re_matches_plain_letter_id():
45
+ """Baseline: ordinary `J.l` form still matches."""
46
+ line = "### Story J.l: update remaining quizazz refs [Done]\n"
47
+ matches = _STORY_RE.findall(line)
48
+ assert matches == [("J.l", "update remaining quizazz refs", "Done")]
49
+
50
+
51
+ def test_story_re_matches_sub_numbered_id():
52
+ """Sub-numbered `J.m.1` form (post-impl follow-up or pre-impl split)
53
+ must be parsed the same way as the plain form."""
54
+ line = "### Story J.m.1: v0.70.0 Follow-up after J.m [Done]\n"
55
+ matches = _STORY_RE.findall(line)
56
+ assert matches == [("J.m.1", "v0.70.0 Follow-up after J.m", "Done")]
57
+
58
+
59
+ def test_story_re_matches_multi_digit_sub_number():
60
+ """Sub-numbers can exceed a single digit (e.g., `.10`, `.11`)."""
61
+ line = "### Story J.m.10: tenth follow-up [Done]\n"
62
+ matches = _STORY_RE.findall(line)
63
+ assert matches == [("J.m.10", "tenth follow-up", "Done")]
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # _read_done_stories — "last [Done]" selection with sub-numbered IDs
68
+ # ---------------------------------------------------------------------------
69
+
70
+
71
+ def test_read_done_stories_scenario_a_subnumber_follows_bare_letter(tmp_path):
72
+ """Scenario A — post-implementation follow-up.
73
+
74
+ Sequence: J.l, J.m, J.m.1 (all Done). J.m was implemented, then a
75
+ follow-up bug/feature surfaced as J.m.1 before proceeding to J.n.
76
+ The wrapper's "last [Done]" must be J.m.1, not J.m or J.l.
77
+ """
78
+ specs = _write(
79
+ tmp_path,
80
+ (
81
+ "### Story J.l: update remaining refs [Done]\n\n"
82
+ "### Story J.m: v0.69.0 Integrate component [Done]\n\n"
83
+ "### Story J.m.1: v0.70.0 Follow-up after J.m [Done]\n"
84
+ ),
85
+ )
86
+ headings = _read_done_stories(str(specs))
87
+ assert headings is not None
88
+ assert [h.story_id for h in headings] == ["J.l", "J.m", "J.m.1"]
89
+ assert headings[-1] == StoryHeading(
90
+ story_id="J.m.1", title="v0.70.0 Follow-up after J.m"
91
+ )
92
+
93
+
94
+ def test_read_done_stories_scenario_b_subnumber_without_bare_letter(tmp_path):
95
+ """Scenario B — pre-implementation split.
96
+
97
+ Sequence: J.l, J.m.1, J.m.2 (all Done). J.m's scope was deemed too
98
+ large before any implementation began, so it was split into J.m.1
99
+ and J.m.2 and the bare J.m heading never existed. The wrapper's
100
+ "last [Done]" must be J.m.2.
101
+ """
102
+ specs = _write(
103
+ tmp_path,
104
+ (
105
+ "### Story J.l: update remaining refs [Done]\n\n"
106
+ "### Story J.m.1: v0.69.0 First half of the split [Done]\n\n"
107
+ "### Story J.m.2: v0.70.0 Second half of the split [Done]\n"
108
+ ),
109
+ )
110
+ headings = _read_done_stories(str(specs))
111
+ assert headings is not None
112
+ assert [h.story_id for h in headings] == ["J.l", "J.m.1", "J.m.2"]
113
+ assert headings[-1].story_id == "J.m.2"
114
+
115
+
116
+ # ---------------------------------------------------------------------------
117
+ # derive_commit_message — round-trip with the cli's commit-subject regex
118
+ # ---------------------------------------------------------------------------
119
+
120
+
121
+ def test_derive_commit_message_with_sub_numbered_id():
122
+ """Output preserves the full sub-numbered ID, including the colon."""
123
+ heading = StoryHeading(story_id="J.m.1", title="v0.70.0 Follow-up after J.m")
124
+ assert derive_commit_message(heading) == "J.m.1: v0.70.0 Follow-up after J.m"
125
+
126
+
127
+ def test_derive_commit_message_round_trip_with_cli_subject_regex():
128
+ """The commit-subject already-committed check (cli._COMMIT_SUBJECT_STORY_ID_RE)
129
+ must recognize the sub-numbered ID it would produce from our own
130
+ `derive_commit_message`. Without this, a J.m.1 commit subject would
131
+ not register in the "committed" set, breaking the wrapper's
132
+ duplicate-detection path even after the stories.md side is fixed.
133
+ """
134
+ from project_guide.cli import _COMMIT_SUBJECT_STORY_ID_RE
135
+
136
+ subject = derive_commit_message(
137
+ StoryHeading(story_id="J.m.1", title="v0.70.0 Follow-up after J.m")
138
+ )
139
+ m = _COMMIT_SUBJECT_STORY_ID_RE.match(subject)
140
+ assert m is not None, f"regex failed to match subject: {subject!r}"
141
+ assert m.group(1) == "J.m.1"
File without changes
File without changes
File without changes
File without changes