@pareto4/pp-core 1.11.1 → 1.12.0

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 (255) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +31 -0
  3. package/agents/pp-code-fixer.md +1 -1
  4. package/agents/pp-debug-session-manager.md +1 -1
  5. package/agents/pp-debugger.md +1 -1
  6. package/agents/pp-executor.md +20 -10
  7. package/agents/pp-intel-updater.md +1 -1
  8. package/agents/pp-phase-researcher.md +1 -1
  9. package/agents/pp-plan-checker.md +5 -5
  10. package/agents/pp-planner.md +10 -24
  11. package/agents/pp-project-researcher.md +1 -1
  12. package/agents/pp-research-synthesizer.md +1 -1
  13. package/agents/pp-ui-researcher.md +1 -1
  14. package/agents/pp-verifier.md +8 -4
  15. package/bin/install.js +429 -82
  16. package/commands/pp-autonomous.md +8 -6
  17. package/commands/pp-discovery-to-build.md +1 -1
  18. package/commands/pp-discovery.md +3 -2
  19. package/commands/pp-discuss-phase.md +2 -2
  20. package/commands/pp-execute-phase.md +3 -1
  21. package/commands/pp-graphify.md +5 -5
  22. package/commands/pp-import.md +1 -1
  23. package/commands/pp-map-codebase.md +1 -1
  24. package/commands/pp-ns-project.md +2 -3
  25. package/commands/pp-ns-workflow.md +1 -1
  26. package/commands/pp-plan-phase.md +2 -1
  27. package/commands/pp-verify-work.md +2 -1
  28. package/gemini-extension.json +1 -1
  29. package/hooks/dist/pp-cursor-session-start.js +2 -2
  30. package/hooks/dist/pp-session-state.sh +1 -1
  31. package/hooks/dist/pp-statusline.js +21 -2
  32. package/hooks/pp-cursor-session-start.js +2 -2
  33. package/hooks/pp-session-state.sh +1 -1
  34. package/hooks/pp-statusline.js +21 -2
  35. package/package.json +18 -5
  36. package/pareto-skills/pp-project-setup/SKILL.md +5 -63
  37. package/pp-core/bin/detect-build-commands.sh +182 -0
  38. package/pp-core/bin/lib/capability-registry.cjs +65 -7
  39. package/pp-core/bin/lib/check-command-router.cjs +79 -38
  40. package/pp-core/bin/lib/clusters.cjs +2 -3
  41. package/pp-core/bin/lib/command-aliases.cjs +36 -0
  42. package/pp-core/bin/lib/command-arg-projection.cjs +99 -1
  43. package/pp-core/bin/lib/commands.cjs +21 -6
  44. package/pp-core/bin/lib/compose-bindings.cjs +70 -6
  45. package/pp-core/bin/lib/config-schema.cjs +16 -0
  46. package/pp-core/bin/lib/config.cjs +142 -27
  47. package/pp-core/bin/lib/core-utils.cjs +43 -7
  48. package/pp-core/bin/lib/discovery-input-check.cjs +218 -0
  49. package/pp-core/bin/lib/discovery-rebaseline.cjs +1372 -0
  50. package/pp-core/bin/lib/discovery-research-track.cjs +979 -0
  51. package/pp-core/bin/lib/discovery-rollup.cjs +476 -0
  52. package/pp-core/bin/lib/discovery-screens.cjs +1995 -0
  53. package/pp-core/bin/lib/discovery-sweep.cjs +390 -0
  54. package/pp-core/bin/lib/discovery-table.cjs +98 -0
  55. package/pp-core/bin/lib/dispatch-plan.cjs +472 -0
  56. package/pp-core/bin/lib/frontmatter.cjs +17 -2
  57. package/pp-core/bin/lib/init-command-router.cjs +8 -5
  58. package/pp-core/bin/lib/init-contract.cjs +93 -4
  59. package/pp-core/bin/lib/init.cjs +124 -134
  60. package/pp-core/bin/lib/install-profiles.cjs +5 -0
  61. package/pp-core/bin/lib/install-run.cjs +536 -0
  62. package/pp-core/bin/lib/io.cjs +15 -0
  63. package/pp-core/bin/lib/loop-resolver.cjs +24 -0
  64. package/pp-core/bin/lib/milestone.cjs +345 -48
  65. package/pp-core/bin/lib/phase-command-router.cjs +23 -1
  66. package/pp-core/bin/lib/phase-locator.cjs +15 -6
  67. package/pp-core/bin/lib/phase-scope.cjs +91 -0
  68. package/pp-core/bin/lib/phase-tier.cjs +467 -0
  69. package/pp-core/bin/lib/phase.cjs +247 -21
  70. package/pp-core/bin/lib/phases-command-router.cjs +2 -0
  71. package/pp-core/bin/lib/plan-graph.cjs +368 -0
  72. package/pp-core/bin/lib/planning-workspace.cjs +53 -0
  73. package/pp-core/bin/lib/platform-bindings.cjs +32 -2
  74. package/pp-core/bin/lib/profile-output.cjs +11 -3
  75. package/pp-core/bin/lib/prohibition-enforcement.cjs +112 -0
  76. package/pp-core/bin/lib/roadmap-parser.cjs +366 -99
  77. package/pp-core/bin/lib/roadmap.cjs +141 -39
  78. package/pp-core/bin/lib/runtime-hooks-surface.cjs +1 -1
  79. package/pp-core/bin/lib/shell-command-projection.cjs +24 -0
  80. package/pp-core/bin/lib/state-command-router.cjs +82 -13
  81. package/pp-core/bin/lib/state-document.cjs +124 -7
  82. package/pp-core/bin/lib/state.cjs +784 -257
  83. package/pp-core/bin/lib/template.cjs +22 -6
  84. package/pp-core/bin/lib/vault-command-router.cjs +57 -6
  85. package/pp-core/bin/lib/vault-practices.cjs +6 -36
  86. package/pp-core/bin/lib/vault-write-back.cjs +20 -4
  87. package/pp-core/bin/lib/vault.cjs +555 -23
  88. package/pp-core/bin/lib/verify.cjs +53 -31
  89. package/pp-core/bin/lib/workstream.cjs +1 -1
  90. package/pp-core/bin/lib/worktree-safety.cjs +26 -15
  91. package/pp-core/bin/portable-timeout.sh +89 -0
  92. package/pp-core/bin/pp-tools.cjs +149 -9
  93. package/pp-core/bin/shared/config-defaults.manifest.json +7 -0
  94. package/pp-core/bin/shared/config-schema.manifest.json +3 -0
  95. package/pp-core/references/autonomous-best-effort.md +461 -0
  96. package/pp-core/references/autonomous-lifecycle.md +146 -0
  97. package/pp-core/references/autonomous-smart-discuss.md +16 -12
  98. package/pp-core/references/checkpoints.md +1 -1
  99. package/pp-core/references/discovery-id-conventions.md +202 -0
  100. package/pp-core/references/discovery-rebaseline.md +254 -0
  101. package/pp-core/references/discovery-research-track.md +283 -0
  102. package/pp-core/references/discovery-traceability.md +400 -0
  103. package/pp-core/references/loop-hook-dispatch.md +7 -0
  104. package/pp-core/references/mandatory-initial-read.md +1 -1
  105. package/pp-core/references/planner-gap-closure.md +1 -0
  106. package/pp-core/references/planner-human-verify-mode.md +1 -1
  107. package/pp-core/references/planner-load-graph-context.md +1 -1
  108. package/pp-core/references/planner-plan-graph.md +66 -0
  109. package/pp-core/references/planner-tdd.md +1 -0
  110. package/pp-core/references/planning-config.md +8 -0
  111. package/pp-core/references/sketch-tooling.md +4 -0
  112. package/pp-core/references/tdd.md +1 -0
  113. package/pp-core/templates/claude-md.md +1 -1
  114. package/pp-core/templates/phase-prompt.md +6 -2
  115. package/pp-core/templates/prototype-review/review.js +303 -0
  116. package/pp-core/templates/state.md +6 -0
  117. package/pp-core/workflows/_runtime-launcher.snippet.sh +1 -1
  118. package/pp-core/workflows/add-backlog.md +1 -1
  119. package/pp-core/workflows/add-phase.md +1 -1
  120. package/pp-core/workflows/add-tests.md +1 -1
  121. package/pp-core/workflows/add-todo.md +1 -1
  122. package/pp-core/workflows/ai-integration-phase.md +2 -2
  123. package/pp-core/workflows/audit-fix.md +12 -16
  124. package/pp-core/workflows/audit-milestone.md +1 -1
  125. package/pp-core/workflows/audit-uat.md +1 -1
  126. package/pp-core/workflows/autonomous.md +68 -306
  127. package/pp-core/workflows/check-todos.md +1 -1
  128. package/pp-core/workflows/cleanup.md +1 -1
  129. package/pp-core/workflows/code-review-fix.md +1 -1
  130. package/pp-core/workflows/code-review.md +19 -7
  131. package/pp-core/workflows/complete-milestone.md +41 -60
  132. package/pp-core/workflows/debug.md +1 -1
  133. package/pp-core/workflows/diagnose-issues.md +1 -1
  134. package/pp-core/workflows/discovery-sprint/gap-analysis.md +140 -0
  135. package/pp-core/workflows/discovery-sprint/input-package.md +140 -0
  136. package/pp-core/workflows/discovery-sprint/prefill.md +92 -0
  137. package/pp-core/workflows/discovery-sprint/prototype.md +257 -0
  138. package/pp-core/workflows/discovery-sprint/rebaseline.md +276 -0
  139. package/pp-core/workflows/discovery-sprint/session-13-gates.md +24 -0
  140. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +92 -0
  141. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +98 -0
  142. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +215 -0
  143. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +146 -0
  144. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +98 -0
  145. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +107 -0
  146. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +98 -0
  147. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +85 -0
  148. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +109 -0
  149. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +119 -0
  150. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +92 -0
  151. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +98 -0
  152. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +205 -0
  153. package/pp-core/workflows/discovery-sprint/templates/context-template.md +266 -0
  154. package/pp-core/workflows/discovery-sprint.md +79 -1714
  155. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +5 -5
  156. package/pp-core/workflows/discovery-to-build/steps/screen-inventory-gate.md +70 -0
  157. package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +54 -0
  158. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +184 -0
  159. package/pp-core/workflows/discovery-to-build.md +21 -238
  160. package/pp-core/workflows/discuss-phase/modes/advisor.md +1 -1
  161. package/pp-core/workflows/discuss-phase/modes/auto.md +1 -1
  162. package/pp-core/workflows/discuss-phase/modes/chain.md +1 -1
  163. package/pp-core/workflows/discuss-phase-assumptions.md +1 -1
  164. package/pp-core/workflows/discuss-phase.md +9 -2
  165. package/pp-core/workflows/do.md +4 -4
  166. package/pp-core/workflows/docs-update.md +1 -1
  167. package/pp-core/workflows/edit-phase.md +1 -1
  168. package/pp-core/workflows/eval-review.md +1 -1
  169. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +45 -0
  170. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +1 -1
  171. package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  172. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +90 -0
  173. package/pp-core/workflows/execute-phase/steps/install-run.md +29 -0
  174. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +23 -0
  175. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +76 -77
  176. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +72 -0
  177. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +5 -5
  178. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +19 -112
  179. package/pp-core/workflows/execute-phase.md +102 -216
  180. package/pp-core/workflows/execute-plan.md +6 -2
  181. package/pp-core/workflows/explore.md +1 -1
  182. package/pp-core/workflows/extract-learnings.md +1 -1
  183. package/pp-core/workflows/forensics.md +4 -4
  184. package/pp-core/workflows/graduation.md +1 -1
  185. package/pp-core/workflows/health.md +1 -1
  186. package/pp-core/workflows/help/modes/brief.md +1 -0
  187. package/pp-core/workflows/help/modes/default.md +2 -1
  188. package/pp-core/workflows/help/modes/full.md +12 -5
  189. package/pp-core/workflows/import.md +1 -1
  190. package/pp-core/workflows/ingest-docs.md +1 -1
  191. package/pp-core/workflows/init.md +18 -2
  192. package/pp-core/workflows/insert-phase.md +1 -1
  193. package/pp-core/workflows/list-workspaces.md +1 -1
  194. package/pp-core/workflows/manager.md +3 -3
  195. package/pp-core/workflows/map-codebase.md +2 -2
  196. package/pp-core/workflows/milestone-summary.md +4 -4
  197. package/pp-core/workflows/mvp-phase.md +1 -1
  198. package/pp-core/workflows/new-milestone.md +7 -8
  199. package/pp-core/workflows/new-project.md +6 -1
  200. package/pp-core/workflows/new-workspace.md +1 -1
  201. package/pp-core/workflows/next.md +2 -2
  202. package/pp-core/workflows/note.md +2 -2
  203. package/pp-core/workflows/pause-work.md +1 -1
  204. package/pp-core/workflows/plan-milestone-gaps.md +1 -1
  205. package/pp-core/workflows/plan-phase/steps/chunked-planning.md +111 -0
  206. package/pp-core/workflows/plan-phase/steps/phase-tier.md +34 -0
  207. package/pp-core/workflows/plan-phase.md +43 -167
  208. package/pp-core/workflows/plan-review-convergence.md +2 -2
  209. package/pp-core/workflows/plant-seed.md +1 -1
  210. package/pp-core/workflows/pr-branch.md +1 -1
  211. package/pp-core/workflows/profile-user.md +1 -1
  212. package/pp-core/workflows/progress.md +3 -3
  213. package/pp-core/workflows/quick.md +3 -1
  214. package/pp-core/workflows/remove-phase.md +1 -1
  215. package/pp-core/workflows/remove-workspace.md +1 -1
  216. package/pp-core/workflows/resume-project.md +6 -12
  217. package/pp-core/workflows/review.md +1 -1
  218. package/pp-core/workflows/scan.md +1 -1
  219. package/pp-core/workflows/secure-phase.md +1 -1
  220. package/pp-core/workflows/settings-advanced.md +1 -1
  221. package/pp-core/workflows/settings-integrations.md +1 -1
  222. package/pp-core/workflows/settings.md +1 -1
  223. package/pp-core/workflows/ship.md +25 -8
  224. package/pp-core/workflows/sketch-wrap-up.md +1 -1
  225. package/pp-core/workflows/sketch.md +2 -1
  226. package/pp-core/workflows/spec-phase.md +1 -1
  227. package/pp-core/workflows/spike-wrap-up.md +1 -1
  228. package/pp-core/workflows/spike.md +1 -1
  229. package/pp-core/workflows/stats.md +2 -2
  230. package/pp-core/workflows/thread.md +1 -1
  231. package/pp-core/workflows/transition.md +5 -9
  232. package/pp-core/workflows/ui-phase.md +23 -2
  233. package/pp-core/workflows/ui-review.md +1 -1
  234. package/pp-core/workflows/ultraplan-phase.md +2 -2
  235. package/pp-core/workflows/validate-phase.md +1 -1
  236. package/pp-core/workflows/vault-write-back.md +1 -1
  237. package/pp-core/workflows/verify-phase.md +29 -26
  238. package/pp-core/workflows/verify-work.md +14 -10
  239. package/scripts/base64-scan.sh +7 -41
  240. package/scripts/changeset/cli.cjs +25 -5
  241. package/scripts/dev-env.cjs +462 -0
  242. package/scripts/lib/allowlist-ratchet.cjs +103 -1
  243. package/scripts/lib/size-budget.cjs +311 -0
  244. package/scripts/lint-neutrality-names.cjs +694 -0
  245. package/scripts/lint-org-coordinates.allowlist.json +35 -0
  246. package/scripts/lint-org-coordinates.cjs +426 -0
  247. package/scripts/lint-skill-deps.cjs +48 -9
  248. package/scripts/lint-test-file-count.cjs +17 -2
  249. package/scripts/prompt-injection-scan.sh +41 -20
  250. package/scripts/sync-manifest-versions.cjs +41 -7
  251. package/scripts/sync-runtime-launcher.cjs +106 -1
  252. package/scripts/update-size-baseline.cjs +274 -30
  253. package/scripts/workflow-size.cjs +61 -9
  254. package/commands/pp-build-handoff.md +0 -46
  255. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +0 -94
@@ -12,8 +12,9 @@
12
12
  * Dependencies (leaf modules only — no loadConfig):
13
13
  * - node:fs / node:path (stdlib)
14
14
  * - ./phase-id.cjs (escapeRegex, phaseMarkdownRegexSource)
15
- * - ./planning-workspace.cjs (planningDir)
15
+ * - ./planning-workspace.cjs (planningDir, milestonePathsFor)
16
16
  * - ./shell-command-projection.cjs (platformReadSync)
17
+ * - ./core-utils.cjs (generateSlugInternal — the phase-dir slug owner)
17
18
  */
18
19
  var __importDefault = (this && this.__importDefault) || function (mod) {
19
20
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -25,7 +26,10 @@ const phaseIdModule = require("./phase-id.cjs");
25
26
  const { escapeRegex, phaseMarkdownRegexSource } = phaseIdModule;
26
27
  // eslint-disable-next-line @typescript-eslint/no-require-imports
27
28
  const planningWorkspace = require("./planning-workspace.cjs");
28
- const { planningDir } = planningWorkspace;
29
+ const { planningDir, milestonePathsFor } = planningWorkspace;
30
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
31
+ const coreUtilsModule = require("./core-utils.cjs");
32
+ const { generateSlugInternal } = coreUtilsModule;
29
33
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
30
34
  // ─── Roadmap milestone scoping ───────────────────────────────────────────────
31
35
  /**
@@ -37,6 +41,25 @@ function stripShippedMilestones(content) {
37
41
  /**
38
42
  * Extract the current milestone section from ROADMAP.md by positive lookup.
39
43
  */
44
+ /**
45
+ * The `<details>` block whose `<summary>` names `version` — the collapsed shape
46
+ * templates/roadmap.md gives a shipped milestone — as [start, end) offsets, or null.
47
+ * One owner for both extractCurrentMilestone (version from STATE.md) and milestoneScope
48
+ * (version asked for), 261007-mmz follow-up 4.
49
+ */
50
+ function summaryDetailsBlock(content, version) {
51
+ const summaryPattern = new RegExp(`<summary[^>]*>([^<]*${escapeRegex(version)}[^<]*)<\\/summary>`, 'i');
52
+ const summaryMatch = content.match(summaryPattern);
53
+ if (!summaryMatch)
54
+ return null;
55
+ const summaryIdx = content.indexOf(summaryMatch[0]);
56
+ const detailsOpenIdx = content.slice(0, summaryIdx).lastIndexOf('<details');
57
+ if (detailsOpenIdx === -1)
58
+ return null;
59
+ const closingMatch = content.slice(detailsOpenIdx).match(/<\/details>/i);
60
+ const end = closingMatch ? detailsOpenIdx + (closingMatch.index ?? 0) + '</details>'.length : content.length;
61
+ return { start: detailsOpenIdx, end };
62
+ }
40
63
  function extractCurrentMilestone(content, cwd) {
41
64
  if (!cwd)
42
65
  return stripShippedMilestones(content);
@@ -62,29 +85,18 @@ function extractCurrentMilestone(content, cwd) {
62
85
  return stripShippedMilestones(content);
63
86
  const escapedVersion = escapeRegex(version);
64
87
  const sectionPattern = new RegExp(`(^#{1,3}\\s+(?!Phase\\s+\\S).*${escapedVersion}\\b[^\\n]*)`, 'gmi');
65
- const summaryPattern = new RegExp(`<summary[^>]*>([^<]*${escapedVersion}[^<]*)<\\/summary>`, 'i');
66
88
  const headingMatches = [...content.matchAll(sectionPattern)];
67
89
  if (headingMatches.length === 0) {
68
- const summaryMatch = content.match(summaryPattern);
69
- if (summaryMatch) {
70
- const summaryIdx = content.indexOf(summaryMatch[0]);
71
- const beforeSummary = content.slice(0, summaryIdx);
72
- const detailsOpenIdx = beforeSummary.lastIndexOf('<details');
73
- if (detailsOpenIdx !== -1) {
74
- const afterDetails = content.slice(detailsOpenIdx);
75
- const closingMatch = afterDetails.match(/<\/details>/i);
76
- const detailsEnd = closingMatch
77
- ? detailsOpenIdx + (closingMatch.index ?? 0) + '</details>'.length
78
- : content.length;
79
- const anyMilestoneOrDetails = /^#{1,3}\s+(?!Phase\s+\S)(?:.*v\d+\.\d+|✅|📋|🚧|🔄)|<details/im;
80
- const firstMilestoneMatch = content.match(anyMilestoneOrDetails);
81
- const preambleCutoff = firstMilestoneMatch ? firstMilestoneMatch.index : detailsOpenIdx;
82
- const preamble = content.slice(0, preambleCutoff)
83
- .replace(/<details>[\s\S]*?<\/details>/gi, '')
84
- .replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
85
- .replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
86
- return preamble + content.slice(detailsOpenIdx, detailsEnd);
87
- }
90
+ const block = summaryDetailsBlock(content, version);
91
+ if (block) {
92
+ const anyMilestoneOrDetails = /^#{1,3}\s+(?!Phase\s+\S)(?:.*v\d+\.\d+|✅|📋|🚧|🔄)|<details/im;
93
+ const firstMilestoneMatch = content.match(anyMilestoneOrDetails);
94
+ const preambleCutoff = firstMilestoneMatch ? firstMilestoneMatch.index : block.start;
95
+ const preamble = content.slice(0, preambleCutoff)
96
+ .replace(/<details>[\s\S]*?<\/details>/gi, '')
97
+ .replace(/^#{2,4}\s*Phase\s+[\w][\w.-]*\s*:[^\n]*(?:\n(?!#{1,6}\s)[^\n]*)*\n?/gim, '')
98
+ .replace(/^#{1,4}\s*Phase Details\b[^\n]*\n?/gim, '');
99
+ return preamble + content.slice(block.start, block.end);
88
100
  }
89
101
  return stripShippedMilestones(content);
90
102
  }
@@ -178,6 +190,71 @@ function replaceInCurrentMilestone(content, pattern, replacement) {
178
190
  const after = content.slice(offset);
179
191
  return before + after.replace(pattern, replacement);
180
192
  }
193
+ // ─── The phase-section-bounded Plans line (D-08-33) ───────────────────────────
194
+ /**
195
+ * A lazy scan that CANNOT leave the phase section it started in.
196
+ *
197
+ * Each step is tempered by a negative lookahead for the next phase heading, so
198
+ * a pattern anchored on `### Phase N` reaches only text that still belongs to
199
+ * phase N. `[\s\S]*?` on its own does not stop at a heading, and both roadmap
200
+ * writers used exactly that: given a phase section with no Plans line, the scan
201
+ * walked into the NEXT phase and wrote there.
202
+ *
203
+ * Owned here, in one place, because that bound is the property both the summary
204
+ * line pattern and `cmdRoadmapUpdatePlanProgress`'s row-insertion anchors need,
205
+ * and three copies of it would be three chances to lose it again (D-08-33).
206
+ *
207
+ * ReDoS shape (T-08-52): one lazy loop over a single character class, guarded
208
+ * by a lookahead with no quantifier of its own beyond the bounded `#{2,4}\s*`.
209
+ * There is no nested quantifier for a hostile roadmap to exploit; the
210
+ * `bug-663-redos-roadmap-phase-parsing` and `feat-3594-parser-adversarial-roadmap`
211
+ * suites are the standing regression net (an elapsed-time assertion is banned by
212
+ * `local/no-elapsed-assertion`).
213
+ */
214
+ const PHASE_SECTION_SCAN = '(?:(?!\\n#{2,4}\\s*Phase\\s)[\\s\\S])*?';
215
+ /**
216
+ * The three Plans-summary-line spellings a ROADMAP.md may use. All are
217
+ * tolerated; the canonical template (`pp-core/templates/roadmap.md`) writes the
218
+ * first:
219
+ * `**Plans**: N plans` — bold word, colon OUTSIDE the bold markers
220
+ * `**Plans:** N plans` — colon INSIDE the bold markers
221
+ * `Plans: N plans` — plain-text header at start of a line
222
+ */
223
+ const PLANS_SUMMARY_SPELLINGS = '(?:\\*\\*Plans\\*\\*:|\\*\\*Plans:\\*\\*|(?:^|\\n)Plans:)';
224
+ /**
225
+ * Build the pattern that locates ONE phase's Plans summary line and captures
226
+ * everything up to (and including) its `Plans:` marker, leaving the line's value
227
+ * as the unmatched tail for the caller to replace.
228
+ *
229
+ * `pattern.replace(p, '$1<new value>')` therefore rewrites the value and nothing
230
+ * else. Group 1 spans from the phase heading to the marker.
231
+ *
232
+ * FOUR properties, each of which one of the two call sites was missing before
233
+ * plan 08-10 (D-08-33):
234
+ *
235
+ * 1. All three spellings. `cmdPhaseComplete` knew only `**Plans:**`, so on a
236
+ * CANONICAL roadmap — `**Plans**:`, colon outside — it did not find its own
237
+ * phase's line and kept scanning.
238
+ * 2. The `(?=[:\s])` phase-number boundary. Without it `Phase 1` matches the
239
+ * heading `Phase 10`; `cmdRoadmapUpdatePlanProgress` had this guard and
240
+ * `cmdPhaseComplete` did not.
241
+ * 3. The phase-section bound (`PHASE_SECTION_SCAN`). This is the F2 defect
242
+ * measured on the live UAT fixture: `phase complete 1` rewrote line 90 of
243
+ * ROADMAP.md — inside `### Phase 2` — to `2/2 plans complete`, asserting a
244
+ * build that had never happened for a phase with nothing executed. The
245
+ * same latent bug sat in `cmdRoadmapUpdatePlanProgress`.
246
+ * 4. A same-line tail (`[^\S\n]*`, horizontal whitespace only). Both sites
247
+ * used `\s*`, which crosses newlines: a bare `Plans:` checklist HEADER on
248
+ * its own line would have had the first checklist row below it consumed and
249
+ * rewritten into a count.
250
+ *
251
+ * Consumers: `cmdRoadmapUpdatePlanProgress` (src/roadmap.cts) and
252
+ * `cmdPhaseComplete` (src/phase.cts). Neither carries its own copy.
253
+ */
254
+ function buildPlansSummaryLinePattern(phaseNum) {
255
+ const phaseSource = phaseMarkdownRegexSource(phaseNum);
256
+ return new RegExp(`(#{2,4}\\s*Phase\\s+${phaseSource}(?=[:\\s])${PHASE_SECTION_SCAN}${PLANS_SUMMARY_SPELLINGS}[^\\S\\n]*)[^\\n]+`, 'i');
257
+ }
181
258
  function findRoadmapPhaseInContent(content, phaseNum) {
182
259
  const phasePattern = new RegExp(`#{2,4}\\s*(?:\\[[^\\]]+\\]\\s*)?Phase\\s+${phaseMarkdownRegexSource(phaseNum)}:\\s*([^\\n]+)`, 'i');
183
260
  const headerMatch = content.match(phasePattern);
@@ -219,7 +296,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
219
296
  return null;
220
297
  }
221
298
  }
222
- function getMilestoneInfo(cwd) {
299
+ function resolveMilestoneInfo(cwd) {
223
300
  try {
224
301
  const roadmap = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'));
225
302
  if (roadmap === null)
@@ -239,45 +316,83 @@ function getMilestoneInfo(cwd) {
239
316
  }
240
317
  if (stateVersion) {
241
318
  const escapedVer = escapeRegex(stateVersion);
242
- const headingMatch = roadmap.match(new RegExp(`##[^\\n]*${escapedVer}[:\\s]+([^\\n(]+)`, 'i'));
319
+ // 261007-ixt W3c: a `### Phase N: vX.Y …` heading names a version without
320
+ // declaring it, so it must not supply the milestone's name.
321
+ const headingMatch = roadmap.match(new RegExp(`^#{2,}\\s+(?!Phase\\s+\\S)[^\\n]*${escapedVer}[:\\s]+([^\\n(]+)`, 'im'));
243
322
  if (headingMatch) {
244
323
  if (!headingMatch[0].includes('✅')) {
245
- return { version: stateVersion, name: headingMatch[1].trim() };
324
+ return { info: { version: stateVersion, name: headingMatch[1].trim() }, declared: true };
246
325
  }
247
326
  }
248
327
  else {
249
328
  const listMatch = roadmap.match(new RegExp(`🚧\\s*\\*?\\*?${escapedVer}\\s+([^*\\n]+)`, 'i'));
250
329
  if (listMatch) {
251
- return { version: stateVersion, name: listMatch[1].trim() };
330
+ return { info: { version: stateVersion, name: listMatch[1].trim() }, declared: true };
331
+ }
332
+ const bodyMatch = roadmap.match(new RegExp(`^\\*\\*\\s*Milestone\\s+${escapedVer}\\s*[—–:-]+\\s*([^*\\n]+?)\\s*\\*\\*`, 'im'));
333
+ if (bodyMatch) {
334
+ return { info: { version: stateVersion, name: bodyMatch[1].trim() }, declared: true };
252
335
  }
253
- return { version: stateVersion, name: 'milestone' };
336
+ return { info: { version: stateVersion, name: 'milestone' }, declared: true };
254
337
  }
255
338
  }
256
339
  const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+(?:\.\d+)+)\s+([^*]+)\*\*/);
257
340
  if (inProgressMatch) {
258
- return {
259
- version: 'v' + inProgressMatch[1],
260
- name: inProgressMatch[2].trim(),
261
- };
341
+ return { info: {
342
+ version: 'v' + inProgressMatch[1],
343
+ name: inProgressMatch[2].trim(),
344
+ }, declared: true };
262
345
  }
263
346
  const cleaned = stripShippedMilestones(roadmap);
264
- const headingMatch = cleaned.match(/## (?!.*✅).*v(\d+(?:\.\d+)+)[:\s]+([^\n(]+)/);
347
+ // 261007-ixt W3c: a PHASE heading that names a version (`### Phase 12: v1.0
348
+ // Carry-Forward Closure`) does not declare that milestone — the same rule
349
+ // getMilestonePhaseFilter's `(?!Phase\s+\S)` lookahead applies (CARRY-04). With no
350
+ // STATE.md to read (health --repair regenerating it), this branch used to name
351
+ // the regenerated v1.1 record `v1.0 Carry-Forward Closure`.
352
+ const headingMatch = cleaned.match(/^#{2,}\s+(?!Phase\s+\S)(?!.*✅).*v(\d+(?:\.\d+)+)[:\s]+([^\n(]+)/m);
265
353
  if (headingMatch) {
266
- return {
267
- version: 'v' + headingMatch[1],
268
- name: headingMatch[2].trim(),
269
- };
354
+ return { info: {
355
+ version: 'v' + headingMatch[1],
356
+ name: headingMatch[2].trim(),
357
+ }, declared: true };
358
+ }
359
+ // A milestone declared in bold body text — `**Milestone v1.1 — Vault Hardening**`,
360
+ // the form CARRY-04 taught getMilestonePhaseFilter to recognise.
361
+ const bodyDeclared = cleaned.match(/^\*\*\s*Milestone\s+v(\d+(?:\.\d+)+)\s*[—–:-]+\s*([^*\n]+?)\s*\*\*/m);
362
+ if (bodyDeclared) {
363
+ return { info: {
364
+ version: 'v' + bodyDeclared[1],
365
+ name: bodyDeclared[2].trim(),
366
+ }, declared: true };
270
367
  }
368
+ // Last resort: nothing DECLARES a current milestone (no STATE.md version, no
369
+ // in-progress marker, no milestone heading, no bold-body declaration). The first
370
+ // version the text mentions is a guess — between milestones it is whichever closed
371
+ // milestone the completed list happens to name first (261007-ixt W4).
271
372
  const versionMatch = cleaned.match(/v(\d+(?:\.\d+)+)/);
272
373
  return {
273
- version: versionMatch ? versionMatch[0] : 'v1.0',
274
- name: 'milestone',
374
+ info: { version: versionMatch ? versionMatch[0] : 'v1.0', name: 'milestone' },
375
+ declared: false,
275
376
  };
276
377
  }
277
378
  catch {
278
- return { version: 'v1.0', name: 'milestone' };
379
+ return { info: { version: 'v1.0', name: 'milestone' }, declared: false };
279
380
  }
280
381
  }
382
+ /**
383
+ * The milestone the planning documents DECLARE as current, or null when nothing
384
+ * declares one (261007-ixt W4). getMilestoneInfo keeps its long-standing last-resort
385
+ * guess for display callers; a writer that stores the milestone in a record must not
386
+ * store a guess — between milestones the guess was whichever closed milestone the
387
+ * ROADMAP's completed list named first.
388
+ */
389
+ function getDeclaredMilestoneInfo(cwd) {
390
+ const resolved = resolveMilestoneInfo(cwd);
391
+ return resolved.declared ? resolved.info : null;
392
+ }
393
+ function getMilestoneInfo(cwd) {
394
+ return resolveMilestoneInfo(cwd).info;
395
+ }
281
396
  // ─── Fence-aware text helper ──────────────────────────────────────────────────
282
397
  /**
283
398
  * Return a copy of `text` with every line that lies inside a fenced code block
@@ -319,6 +434,90 @@ function stripFencedLines(text) {
319
434
  }
320
435
  return result.join('\n');
321
436
  }
437
+ /**
438
+ * 261007-mmz follow-up 3: the ONE owner of "which part of a ROADMAP text belongs to
439
+ * milestone <version>". Extracted verbatim from getMilestonePhaseFilter (which now
440
+ * calls it), so `milestone complete` and the closed-milestone snapshot reader scope a
441
+ * text the same way. With no versionOverride the scope is the current milestone
442
+ * (STATE.md-keyed when cwd is given, otherwise the document minus shipped blocks).
443
+ */
444
+ function milestoneScope(roadmapContent, versionOverride, cwd) {
445
+ // 261007-mmz follow-up 4: when a version is asked for, STATE.md is never consulted —
446
+ // the answer depends on the text and the version only, so `milestone complete vX`
447
+ // (cwd) and the archived-snapshot reader (no cwd) scope the same text the same way.
448
+ // STATE.md keys only the no-version "current milestone" question.
449
+ let roadmap = versionOverride ? extractCurrentMilestone(roadmapContent) : extractCurrentMilestone(roadmapContent, cwd);
450
+ let missingExplicitVersion = false;
451
+ if (versionOverride) {
452
+ const escapedVersion = escapeRegex(versionOverride);
453
+ const sectionPattern = new RegExp(`(^#{1,3}\\s+(?!Phase\\s+\\S).*${escapedVersion}[^\\n]*)`, 'mi');
454
+ const sectionMatch = roadmapContent.match(sectionPattern);
455
+ // A milestone declared only in a <summary> (the collapsed templates/roadmap.md shape)
456
+ // is scoped to ITS <details> block. This used to fall through to the current-milestone
457
+ // read: with cwd, STATE.md's milestone (v1.0 → 1-4, but v1.1 → v1.1's phases); without
458
+ // cwd, the document minus shipped blocks — i.e. everything EXCEPT the requested
459
+ // milestone (measured: v1.0's snapshot owned {5}, not {1..4}).
460
+ const summaryBlock = sectionMatch ? null : summaryDetailsBlock(roadmapContent, versionOverride);
461
+ if (summaryBlock) {
462
+ roadmap = roadmapContent.slice(summaryBlock.start, summaryBlock.end);
463
+ }
464
+ else if (!sectionMatch) {
465
+ const hasVersionedMilestones = /^#{1,3}\s+(?!Phase\s+\S).*v\d+\.\d+/mi.test(roadmapContent);
466
+ // A milestone is not always declared in a HEADING. The vault-assurance
467
+ // workstream declares its milestone on line 3 of its ROADMAP in bold
468
+ // body text — `**Milestone v1.1 — Vault Hardening** · started …` — while
469
+ // its only version-bearing heading is `### Phase 12: v1.0 …`, which the
470
+ // `(?!Phase\s+\S)` lookahead excludes ON PURPOSE: a phase heading that
471
+ // merely NAMES a version does not declare that milestone. Before phase
472
+ // 12 that combination left hasVersionedMilestones false for every
473
+ // requested version, so the guard could never fire, `roadmap` stayed
474
+ // scoped to the CURRENT milestone, and `milestone complete v1.0`
475
+ // archived the current milestone's phases under a v1.0 label and
476
+ // exited 0. Recognising the bold-body form is what closes CARRY-04.
477
+ const declaresBodyMilestone = /^\*\*\s*Milestone\s+v\d+\.\d+/mi.test(roadmapContent);
478
+ const bodyDeclaresRequested = new RegExp(`^\\*\\*\\s*Milestone\\s+${escapedVersion}\\b`, 'mi').test(roadmapContent);
479
+ const versionInSummary = new RegExp(`<summary[^>]*>[^<]*${escapedVersion}[^<]*<\\/summary>`, 'i').test(roadmapContent);
480
+ if ((hasVersionedMilestones || declaresBodyMilestone) && !versionInSummary && !bodyDeclaresRequested) {
481
+ roadmap = '';
482
+ missingExplicitVersion = true;
483
+ }
484
+ }
485
+ else {
486
+ const sectionStart = sectionMatch.index;
487
+ const headingLevel = (sectionMatch[1].match(/^(#{1,3})\s/) ?? ['', '#'])[1].length;
488
+ const restContent = roadmapContent.slice(sectionStart + sectionMatch[0].length);
489
+ const nextMilestonePattern = new RegExp(`^#{1,${headingLevel}}\\s+(?!Phase\\s+\\S)(?:.*v\\d+\\.\\d+|✅|📋|🚧)`, 'i');
490
+ let sectionEnd = roadmapContent.length;
491
+ let fenceChar = null;
492
+ let fenceLen = 0;
493
+ let charOffset = 0;
494
+ for (const line of restContent.split('\n')) {
495
+ const fenceMatch = line.match(/^\s{0,3}((?:`{3,}|~{3,}))(.*)/);
496
+ if (fenceMatch) {
497
+ const char = fenceMatch[1][0];
498
+ const len = fenceMatch[1].length;
499
+ const trailing = fenceMatch[2] || '';
500
+ if (!fenceChar) {
501
+ fenceChar = char;
502
+ fenceLen = len;
503
+ }
504
+ else if (char === fenceChar && len >= fenceLen && /^\s*$/.test(trailing)) {
505
+ fenceChar = null;
506
+ fenceLen = 0;
507
+ }
508
+ }
509
+ else if (!fenceChar && nextMilestonePattern.test(line)) {
510
+ sectionEnd = sectionStart + sectionMatch[0].length + charOffset;
511
+ break;
512
+ }
513
+ charOffset += line.length + 1;
514
+ }
515
+ const currentSection = roadmapContent.slice(sectionStart, sectionEnd);
516
+ roadmap = currentSection;
517
+ }
518
+ }
519
+ return { scope: roadmap, missingExplicitVersion };
520
+ }
322
521
  /**
323
522
  * Returns a filter function that checks whether a phase directory belongs
324
523
  * to the current milestone based on ROADMAP.md phase headings.
@@ -335,12 +534,18 @@ function stripFencedLines(text) {
335
534
  function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
336
535
  const milestonePhaseNums = new Set();
337
536
  let missingExplicitVersion = false;
537
+ let roadmapMissing = false;
338
538
  try {
339
539
  const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
340
540
  const roadmapContent = (0, shell_command_projection_cjs_1.platformReadSync)(roadmapPath);
341
- if (roadmapContent === null)
541
+ if (roadmapContent === null) {
542
+ // The catch at the end of this block swallows the throw and the tail
543
+ // returns a pass-all filter. That stays as it is — the FLAG, not the
544
+ // catch, is what now carries the information out, so every caller's
545
+ // control flow is byte-identical (phase 12, CARRY-04).
546
+ roadmapMissing = true;
342
547
  throw new Error('missing');
343
- let roadmap = extractCurrentMilestone(roadmapContent, cwd);
548
+ }
344
549
  const hasVersionedMilestonesGlobal = /^#{1,3}\s+.*v\d+\.\d+/mi.test(roadmapContent);
345
550
  const hasPhaseHeadings = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+[\w]/i.test(roadmapContent);
346
551
  if (!hasVersionedMilestonesGlobal && hasPhaseHeadings && phaseIdConvention === 'milestone-prefixed') {
@@ -348,63 +553,9 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
348
553
  'The project has phase_id_convention set to "milestone-prefixed" in config.json but the ' +
349
554
  'ROADMAP does not use versioned milestone headings. Run `pp-tools roadmap upgrade --convention milestone-prefixed` to migrate (dry-run by default).');
350
555
  }
351
- if (versionOverride) {
352
- const escapedVersion = escapeRegex(versionOverride);
353
- const sectionPattern = new RegExp(`(^#{1,3}\\s+(?!Phase\\s+\\S).*${escapedVersion}[^\\n]*)`, 'mi');
354
- let sectionMatch = roadmapContent.match(sectionPattern);
355
- if (!sectionMatch) {
356
- const summaryPat = new RegExp(`<summary[^>]*>[^<]*${escapedVersion}[^<]*<\\/summary>`, 'i');
357
- const summaryHit = roadmapContent.match(summaryPat);
358
- if (summaryHit) {
359
- const beforeSummary = roadmapContent.slice(0, summaryHit.index);
360
- const detailsIdx = beforeSummary.lastIndexOf('<details');
361
- if (detailsIdx !== -1) {
362
- sectionMatch = null;
363
- }
364
- }
365
- }
366
- if (!sectionMatch) {
367
- const hasVersionedMilestones = /^#{1,3}\s+(?!Phase\s+\S).*v\d+\.\d+/mi.test(roadmapContent);
368
- const versionInSummary = new RegExp(`<summary[^>]*>[^<]*${escapedVersion}[^<]*<\\/summary>`, 'i').test(roadmapContent);
369
- if (hasVersionedMilestones && !versionInSummary) {
370
- roadmap = '';
371
- missingExplicitVersion = true;
372
- }
373
- }
374
- else {
375
- const sectionStart = sectionMatch.index;
376
- const headingLevel = (sectionMatch[1].match(/^(#{1,3})\s/) ?? ['', '#'])[1].length;
377
- const restContent = roadmapContent.slice(sectionStart + sectionMatch[0].length);
378
- const nextMilestonePattern = new RegExp(`^#{1,${headingLevel}}\\s+(?!Phase\\s+\\S)(?:.*v\\d+\\.\\d+|✅|📋|🚧)`, 'i');
379
- let sectionEnd = roadmapContent.length;
380
- let fenceChar = null;
381
- let fenceLen = 0;
382
- let charOffset = 0;
383
- for (const line of restContent.split('\n')) {
384
- const fenceMatch = line.match(/^\s{0,3}((?:`{3,}|~{3,}))(.*)/);
385
- if (fenceMatch) {
386
- const char = fenceMatch[1][0];
387
- const len = fenceMatch[1].length;
388
- const trailing = fenceMatch[2] || '';
389
- if (!fenceChar) {
390
- fenceChar = char;
391
- fenceLen = len;
392
- }
393
- else if (char === fenceChar && len >= fenceLen && /^\s*$/.test(trailing)) {
394
- fenceChar = null;
395
- fenceLen = 0;
396
- }
397
- }
398
- else if (!fenceChar && nextMilestonePattern.test(line)) {
399
- sectionEnd = sectionStart + sectionMatch[0].length + charOffset;
400
- break;
401
- }
402
- charOffset += line.length + 1;
403
- }
404
- const currentSection = roadmapContent.slice(sectionStart, sectionEnd);
405
- roadmap = currentSection;
406
- }
407
- }
556
+ const scoped = milestoneScope(roadmapContent, versionOverride, cwd);
557
+ const roadmap = scoped.scope;
558
+ missingExplicitVersion = scoped.missingExplicitVersion;
408
559
  const phasePattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+([\w][\w.-]*)\s*:/gi;
409
560
  const roadmapUnfenced = stripFencedLines(roadmap);
410
561
  let m;
@@ -417,6 +568,7 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
417
568
  const passAll = (() => true);
418
569
  passAll.phaseCount = 0;
419
570
  passAll.missingExplicitVersion = missingExplicitVersion;
571
+ passAll.roadmapMissing = roadmapMissing;
420
572
  return passAll;
421
573
  }
422
574
  const normalized = new Set([...milestonePhaseNums].map(n => n.split('-').map(seg => (seg.replace(/^0+(?=\d)/, '') || '0')).join('-').toLowerCase()));
@@ -444,13 +596,128 @@ function getMilestonePhaseFilter(cwd, versionOverride, phaseIdConvention) {
444
596
  }
445
597
  isDirInMilestone.phaseCount = milestonePhaseNums.size;
446
598
  isDirInMilestone.missingExplicitVersion = missingExplicitVersion;
599
+ isDirInMilestone.roadmapMissing = roadmapMissing;
447
600
  return isDirInMilestone;
448
601
  }
602
+ /**
603
+ * `<number>:<slug>` keys for every `Phase N: Name` heading in `text` — the identity a
604
+ * phase directory carries in its own name (`01-release-pipeline`), number normalised
605
+ * (leading zeros dropped) and name slugged by the same owner that names directories.
606
+ */
607
+ function phaseHeadingKeys(text) {
608
+ const keys = new Set();
609
+ const pattern = /#{2,4}\s*(?:\[[^\]]+\]\s*)?Phase\s+(\d+[A-Za-z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
610
+ let m;
611
+ const unfenced = stripFencedLines(text);
612
+ while ((m = pattern.exec(unfenced)) !== null) {
613
+ const slug = generateSlugInternal(m[2]);
614
+ if (slug)
615
+ keys.add(`${m[1].replace(/^0+(?=\d)/, '').toLowerCase()}:${slug}`);
616
+ }
617
+ return keys;
618
+ }
619
+ function phaseDirKey(dirName) {
620
+ const m = dirName.match(/^0*(\d+[A-Za-z]?(?:\.\d+)*)-(.+)$/);
621
+ return m ? `${(m[1] || '0').toLowerCase()}:${m[2].toLowerCase()}` : null;
622
+ }
623
+ /**
624
+ * 261007-ixt D-9: which directories under `phases/` belong to a CLOSED milestone.
625
+ *
626
+ * A directory is closed when an archived `milestones/vX-ROADMAP.md` (the snapshot
627
+ * `milestone complete` writes) lists a phase with the same number AND the same name,
628
+ * and the current milestone's ROADMAP scope does not list it. Number alone is not
629
+ * enough — a later milestone may reuse numbers under new names — and a phase the
630
+ * current scope still names is still the record's milestone (milestone complete
631
+ * without --archive-phases leaves both the snapshot and the directories in place).
632
+ *
633
+ * The milestone filter cannot see this: when the current milestone names no phase
634
+ * (between milestones), it passes every directory, and the progress scan counted a
635
+ * closed milestone's leftover directories as the current milestone's (measured on the
636
+ * live vault-assurance tree: v1.0's 01–07 rewrote the v1.1 block 5/5/39/39 to
637
+ * 7/7/119/120). No archive on disk → nothing is closed, so a single-milestone
638
+ * project's checklist-only ROADMAP keeps counting every directory.
639
+ */
640
+ function getClosedMilestonePhaseDirFilter(cwd) {
641
+ const owners = getClosedMilestonePhaseDirOwners(cwd);
642
+ return (dirName) => owners(dirName).length > 0;
643
+ }
644
+ /**
645
+ * 261007-mmz: the same predicate as getClosedMilestonePhaseDirFilter, answering WHICH
646
+ * closed milestones name a directory — the versions whose archived snapshot lists the
647
+ * directory's number AND name, sorted, minus any phase the current milestone's scope
648
+ * still lists. The filter above is `owners(dir).length > 0`, so the two can never
649
+ * disagree; `phases archive-closed` reads the versions to move a leftover directory
650
+ * into the one milestone archive that names it, and leaves it when none or several do.
651
+ */
652
+ /**
653
+ * 261007-mmz follow-up 3: the phases an archived `<version>-ROADMAP.md` snapshot names
654
+ * for ITS OWN milestone. A snapshot is a copy of the whole ROADMAP at close time, so it
655
+ * also lists the next milestone's phases; reading every heading made v1.0's snapshot
656
+ * claim v1.1's phase 5 (measured on the step-walked fixture: `phases archive-closed`
657
+ * moved 05-security-audit into v1.0-phases while `milestone complete v1.0` had scoped
658
+ * 1-4). The scope comes from milestoneScope — the same owner `milestone complete`
659
+ * uses — read without cwd, because STATE.md no longer describes a closed milestone.
660
+ *
661
+ * A snapshot in which that scope names no phase for its own version is one the fixed
662
+ * verb would have refused to write (D-1/D-3), so only a close made before 261007-mmz
663
+ * produces it — the live vault-assurance v1.0 snapshot is one: a `### … whole of v1.0`
664
+ * heading in its Open Work section captures the section match. For those the reading
665
+ * stays the pre-existing whole-snapshot one, which is what 261007-ixt D-9 measured on
666
+ * the live tree; number AND name must still match.
667
+ */
668
+ function snapshotPhaseKeys(text, version) {
669
+ const scoped = phaseHeadingKeys(milestoneScope(text, version).scope);
670
+ return scoped.size > 0 ? scoped : phaseHeadingKeys(text);
671
+ }
672
+ function getClosedMilestonePhaseDirOwners(cwd) {
673
+ const byKey = new Map();
674
+ try {
675
+ const archiveDir = milestonePathsFor(planningDir(cwd)).milestonesArchive;
676
+ for (const file of node_fs_1.default.readdirSync(archiveDir).sort()) {
677
+ const vm = file.match(/^(v\d+(?:\.\d+)*)-ROADMAP\.md$/i);
678
+ if (!vm)
679
+ continue;
680
+ const text = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(archiveDir, file));
681
+ if (text === null)
682
+ continue;
683
+ for (const key of snapshotPhaseKeys(text, vm[1])) {
684
+ const versions = byKey.get(key) ?? [];
685
+ if (!versions.includes(vm[1]))
686
+ versions.push(vm[1]);
687
+ byKey.set(key, versions);
688
+ }
689
+ }
690
+ }
691
+ catch { /* no archive directory: nothing is closed */ }
692
+ if (byKey.size === 0)
693
+ return () => [];
694
+ try {
695
+ const roadmapRaw = (0, shell_command_projection_cjs_1.platformReadSync)(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'));
696
+ if (roadmapRaw !== null) {
697
+ for (const key of phaseHeadingKeys(extractCurrentMilestone(roadmapRaw, cwd)))
698
+ byKey.delete(key);
699
+ }
700
+ }
701
+ catch { /* unreadable ROADMAP: the archive alone decides */ }
702
+ return (dirName) => {
703
+ const key = phaseDirKey(dirName);
704
+ return key !== null ? [...(byKey.get(key) ?? [])] : [];
705
+ };
706
+ }
449
707
  module.exports = {
708
+ getClosedMilestonePhaseDirFilter,
709
+ getClosedMilestonePhaseDirOwners,
450
710
  stripShippedMilestones,
451
711
  extractCurrentMilestone,
452
712
  replaceInCurrentMilestone,
713
+ buildPlansSummaryLinePattern,
714
+ // Exported for `cmdRoadmapUpdatePlanProgress`'s row-INSERTION anchors, which
715
+ // need the same "cannot cross a phase heading" bound but a different tail and
716
+ // a different alternation. One owner for the bound, two shapes built from it.
717
+ PHASE_SECTION_SCAN,
453
718
  getRoadmapPhaseInternal,
454
719
  getMilestoneInfo,
720
+ getDeclaredMilestoneInfo,
455
721
  getMilestonePhaseFilter,
722
+ milestoneScope,
456
723
  };