@opengsd/gsd-core 1.12.0 → 1.13.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 (286) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +12 -0
  4. package/agents/gsd-executor.md +63 -35
  5. package/agents/gsd-plan-checker.md +76 -57
  6. package/agents/gsd-planner.md +14 -0
  7. package/agents/gsd-ui-checker.md +19 -3
  8. package/agents/gsd-ui-researcher.md +29 -0
  9. package/agents/gsd-verifier.md +23 -1
  10. package/bin/install.js +239 -67
  11. package/commands/gsd/execute-phase.md +1 -1
  12. package/commands/gsd/ns-workflow.md +2 -1
  13. package/commands/gsd/phase.md +1 -1
  14. package/commands/gsd/quick-batch.md +105 -0
  15. package/commands/gsd/surface.md +18 -8
  16. package/gsd-core/bin/gsd-tools.cjs +195 -50
  17. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  18. package/gsd-core/bin/lib/capability-registry.cjs +514 -114
  19. package/gsd-core/bin/lib/capability-state.cjs +7 -1
  20. package/gsd-core/bin/lib/capability-validator.cjs +120 -4
  21. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  22. package/gsd-core/bin/lib/check-command-router.cjs +85 -2
  23. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  24. package/gsd-core/bin/lib/clusters.cjs +1 -0
  25. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  26. package/gsd-core/bin/lib/commands.cjs +337 -13
  27. package/gsd-core/bin/lib/config-loader.cjs +3 -0
  28. package/gsd-core/bin/lib/core-utils.cjs +34 -7
  29. package/gsd-core/bin/lib/decisions.cjs +213 -1
  30. package/gsd-core/bin/lib/edge-probe.cjs +14 -1
  31. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  32. package/gsd-core/bin/lib/frontmatter.cjs +137 -23
  33. package/gsd-core/bin/lib/gap-checker.cjs +22 -13
  34. package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
  35. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  36. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
  37. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
  38. package/gsd-core/bin/lib/host-integration.cjs +57 -5
  39. package/gsd-core/bin/lib/init-command-router.cjs +14 -0
  40. package/gsd-core/bin/lib/init.cjs +132 -15
  41. package/gsd-core/bin/lib/install-engine.cjs +184 -12
  42. package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
  43. package/gsd-core/bin/lib/install-profiles.cjs +22 -14
  44. package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
  45. package/gsd-core/bin/lib/io.cjs +35 -0
  46. package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
  47. package/gsd-core/bin/lib/markdown-table.cjs +123 -0
  48. package/gsd-core/bin/lib/milestone.cjs +22 -2
  49. package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
  50. package/gsd-core/bin/lib/phase-id.cjs +251 -9
  51. package/gsd-core/bin/lib/phase.cjs +774 -35
  52. package/gsd-core/bin/lib/plan-document.cjs +10 -0
  53. package/gsd-core/bin/lib/planning-snapshot.cjs +147 -20
  54. package/gsd-core/bin/lib/planning-workspace.cjs +103 -28
  55. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  56. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  57. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  58. package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
  59. package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
  60. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  61. package/gsd-core/bin/lib/roadmap-parser.cjs +499 -26
  62. package/gsd-core/bin/lib/roadmap.cjs +187 -58
  63. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +233 -33
  64. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
  65. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +286 -108
  66. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -43
  67. package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
  68. package/gsd-core/bin/lib/smart-entry.cjs +7 -9
  69. package/gsd-core/bin/lib/state-document.cjs +30 -5
  70. package/gsd-core/bin/lib/state-md-schema.cjs +23 -13
  71. package/gsd-core/bin/lib/state-transition.cjs +333 -44
  72. package/gsd-core/bin/lib/state.cjs +684 -125
  73. package/gsd-core/bin/lib/surface.cjs +23 -8
  74. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  75. package/gsd-core/bin/lib/uat.cjs +1419 -515
  76. package/gsd-core/bin/lib/update-context.cjs +6 -2
  77. package/gsd-core/bin/lib/validate.cjs +230 -12
  78. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  79. package/gsd-core/bin/lib/verification.cjs +273 -12
  80. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  81. package/gsd-core/bin/lib/verify.cjs +346 -16
  82. package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
  83. package/gsd-core/bin/lib/worktree-safety.cjs +8 -0
  84. package/gsd-core/bin/shared/config-schema.manifest.json +8 -0
  85. package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
  86. package/gsd-core/references/agent-contracts.md +3 -3
  87. package/gsd-core/references/edge-probe.md +17 -13
  88. package/gsd-core/references/execute-mvp-tdd.md +18 -16
  89. package/gsd-core/references/execute-phase-response-language.md +6 -0
  90. package/gsd-core/references/executor-examples.md +42 -0
  91. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  92. package/gsd-core/references/mvp-concepts.md +2 -2
  93. package/gsd-core/references/plan-checker-examples.md +41 -0
  94. package/gsd-core/references/planner-antipatterns.md +25 -0
  95. package/gsd-core/references/planner-chunked.md +5 -1
  96. package/gsd-core/references/planner-coupling.md +42 -0
  97. package/gsd-core/references/planner-quick-batch.md +71 -0
  98. package/gsd-core/references/planner-reviews.md +47 -0
  99. package/gsd-core/references/planner-revision.md +75 -2
  100. package/gsd-core/references/planning-config.md +2 -1
  101. package/gsd-core/references/response-language-directive.md +9 -0
  102. package/gsd-core/references/revision-loop.md +118 -11
  103. package/gsd-core/references/tdd.md +14 -9
  104. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  105. package/gsd-core/templates/phase-prompt.md +4 -0
  106. package/gsd-core/templates/verification-report.md +5 -0
  107. package/gsd-core/workflows/add-backlog.md +2 -0
  108. package/gsd-core/workflows/add-phase.md +2 -0
  109. package/gsd-core/workflows/add-tests.md +1 -1
  110. package/gsd-core/workflows/add-todo.md +1 -1
  111. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  112. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  113. package/gsd-core/workflows/audit-fix.md +2 -0
  114. package/gsd-core/workflows/audit-milestone.md +2 -0
  115. package/gsd-core/workflows/audit-uat.md +2 -0
  116. package/gsd-core/workflows/autonomous.md +2 -0
  117. package/gsd-core/workflows/check-todos.md +1 -1
  118. package/gsd-core/workflows/cleanup.md +1 -1
  119. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +15 -13
  120. package/gsd-core/workflows/code-review-fix.md +2 -0
  121. package/gsd-core/workflows/code-review.md +73 -31
  122. package/gsd-core/workflows/complete-milestone.md +13 -4
  123. package/gsd-core/workflows/debug.md +1 -1
  124. package/gsd-core/workflows/diagnose-issues.md +5 -1
  125. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
  126. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  127. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  128. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  129. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  130. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
  131. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  132. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  133. package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
  134. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  135. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  136. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  137. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  138. package/gsd-core/workflows/discuss-phase.md +1 -1
  139. package/gsd-core/workflows/do.md +43 -13
  140. package/gsd-core/workflows/docs-update.md +1 -1
  141. package/gsd-core/workflows/edit-phase.md +2 -0
  142. package/gsd-core/workflows/eval-review.md +1 -1
  143. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +2 -0
  144. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +17 -1
  145. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
  146. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
  147. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  148. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  149. package/gsd-core/workflows/execute-phase.md +32 -14
  150. package/gsd-core/workflows/execute-plan.md +8 -8
  151. package/gsd-core/workflows/explore.md +2 -0
  152. package/gsd-core/workflows/extract-learnings.md +2 -0
  153. package/gsd-core/workflows/fast.md +6 -0
  154. package/gsd-core/workflows/forensics.md +2 -0
  155. package/gsd-core/workflows/graduation.md +1 -1
  156. package/gsd-core/workflows/health.md +1 -1
  157. package/gsd-core/workflows/help/modes/brief.md +2 -0
  158. package/gsd-core/workflows/help/modes/default.md +2 -0
  159. package/gsd-core/workflows/help/modes/full.md +12 -0
  160. package/gsd-core/workflows/help/modes/topic.md +2 -0
  161. package/gsd-core/workflows/help.md +2 -0
  162. package/gsd-core/workflows/import.md +3 -3
  163. package/gsd-core/workflows/inbox.md +1 -1
  164. package/gsd-core/workflows/ingest-docs.md +1 -1
  165. package/gsd-core/workflows/insert-phase.md +2 -0
  166. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  167. package/gsd-core/workflows/list-seeds.md +2 -0
  168. package/gsd-core/workflows/list-workspaces.md +2 -0
  169. package/gsd-core/workflows/manager.md +3 -3
  170. package/gsd-core/workflows/map-codebase.md +2 -0
  171. package/gsd-core/workflows/milestone-summary.md +2 -0
  172. package/gsd-core/workflows/mvp-phase.md +1 -1
  173. package/gsd-core/workflows/new-milestone.md +1 -1
  174. package/gsd-core/workflows/new-project.md +5 -3
  175. package/gsd-core/workflows/new-workspace.md +1 -1
  176. package/gsd-core/workflows/next.md +2 -0
  177. package/gsd-core/workflows/node-repair.md +2 -0
  178. package/gsd-core/workflows/note.md +2 -0
  179. package/gsd-core/workflows/onboard.md +1 -1
  180. package/gsd-core/workflows/pause-work.md +19 -4
  181. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  182. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
  183. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
  184. package/gsd-core/workflows/plan-phase.md +130 -12
  185. package/gsd-core/workflows/plan-review-convergence.md +102 -10
  186. package/gsd-core/workflows/plant-seed.md +1 -1
  187. package/gsd-core/workflows/pr-branch.md +11 -3
  188. package/gsd-core/workflows/profile-user.md +1 -1
  189. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  190. package/gsd-core/workflows/progress.md +25 -3
  191. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
  192. package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
  193. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  194. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  195. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  196. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  197. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  198. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  199. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  200. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  201. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  202. package/gsd-core/workflows/quick-batch.md +203 -0
  203. package/gsd-core/workflows/quick.md +13 -3
  204. package/gsd-core/workflows/reapply-patches.md +2 -0
  205. package/gsd-core/workflows/remove-phase.md +2 -0
  206. package/gsd-core/workflows/remove-workspace.md +1 -1
  207. package/gsd-core/workflows/resume-project.md +6 -2
  208. package/gsd-core/workflows/review.md +215 -10
  209. package/gsd-core/workflows/scan.md +2 -0
  210. package/gsd-core/workflows/section-manifest.json +12 -0
  211. package/gsd-core/workflows/secure-phase.md +1 -1
  212. package/gsd-core/workflows/session-report.md +2 -0
  213. package/gsd-core/workflows/settings-advanced.md +2 -0
  214. package/gsd-core/workflows/settings-integrations.md +9 -8
  215. package/gsd-core/workflows/settings.md +1 -1
  216. package/gsd-core/workflows/ship.md +10 -10
  217. package/gsd-core/workflows/sketch-wrap-up.md +2 -0
  218. package/gsd-core/workflows/sketch.md +1 -1
  219. package/gsd-core/workflows/smart-entry.md +1 -1
  220. package/gsd-core/workflows/spec-phase.md +24 -19
  221. package/gsd-core/workflows/spike-wrap-up.md +2 -0
  222. package/gsd-core/workflows/spike.md +1 -1
  223. package/gsd-core/workflows/stats.md +2 -0
  224. package/gsd-core/workflows/sync-skills.md +12 -4
  225. package/gsd-core/workflows/thread.md +2 -0
  226. package/gsd-core/workflows/transition.md +2 -0
  227. package/gsd-core/workflows/ui-phase.md +26 -5
  228. package/gsd-core/workflows/ui-review.md +1 -1
  229. package/gsd-core/workflows/ultraplan-phase.md +2 -0
  230. package/gsd-core/workflows/undo.md +1 -1
  231. package/gsd-core/workflows/update.md +41 -38
  232. package/gsd-core/workflows/validate-phase.md +1 -1
  233. package/gsd-core/workflows/verify-work.md +49 -3
  234. package/hooks/dist/gsd-check-update-worker.js +19 -2
  235. package/hooks/dist/gsd-context-monitor.js +283 -12
  236. package/hooks/dist/gsd-node-runner.sh +1 -0
  237. package/hooks/dist/gsd-prompt-guard.js +30 -5
  238. package/hooks/dist/gsd-read-guard.js +2 -0
  239. package/hooks/dist/gsd-read-injection-scanner.js +5 -5
  240. package/hooks/dist/gsd-secret-read-guard.js +1079 -0
  241. package/hooks/dist/gsd-statusline.js +7 -3
  242. package/hooks/dist/gsd-validate-commit.sh +444 -7
  243. package/hooks/dist/gsd-workflow-guard.js +2 -1
  244. package/hooks/dist/lib/git-cmd.js +210 -1
  245. package/hooks/dist/lib/injection-patterns.js +36 -6
  246. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  247. package/hooks/gsd-check-update-worker.js +19 -2
  248. package/hooks/gsd-context-monitor.js +283 -12
  249. package/hooks/gsd-node-runner.sh +1 -0
  250. package/hooks/gsd-prompt-guard.js +30 -5
  251. package/hooks/gsd-read-guard.js +2 -0
  252. package/hooks/gsd-read-injection-scanner.js +5 -5
  253. package/hooks/gsd-secret-read-guard.js +1079 -0
  254. package/hooks/gsd-statusline.js +7 -3
  255. package/hooks/gsd-validate-commit.sh +444 -7
  256. package/hooks/gsd-workflow-guard.js +2 -1
  257. package/hooks/hooks.json +6 -0
  258. package/hooks/lib/git-cmd.js +210 -1
  259. package/hooks/lib/injection-patterns.js +36 -6
  260. package/hooks/managed-hooks-registry.cjs +1 -0
  261. package/package.json +5 -5
  262. package/scripts/build-hooks.js +11 -4
  263. package/scripts/ci-test-scope.cjs +7 -0
  264. package/scripts/docs-guard-registry.cjs +10 -0
  265. package/scripts/gen-loop-host-contract.cjs +67 -15
  266. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  267. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  268. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  269. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  270. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +5 -0
  271. package/scripts/lint-phase-enumeration-drift.cjs +24 -6
  272. package/scripts/lint-phase-id-drift.cjs +133 -8
  273. package/scripts/lint-portable-grep.cjs +176 -0
  274. package/scripts/lint-response-language-coverage.cjs +524 -0
  275. package/scripts/lint-test-file-count.allowlist.json +3 -1
  276. package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
  277. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  278. package/scripts/npm-audit-baseline.cjs +376 -0
  279. package/scripts/prompt-injection-scan.sh +8 -0
  280. package/scripts/require-issue-link-policy.cjs +16 -1
  281. package/skills/gsd-execute-phase/SKILL.md +1 -1
  282. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  283. package/skills/gsd-phase/SKILL.md +1 -1
  284. package/skills/gsd-quick-batch/SKILL.md +105 -0
  285. package/skills/gsd-surface/SKILL.md +18 -8
  286. package/vscode/package.json +1 -1
@@ -158,7 +158,156 @@ const BRACKET_PHASE_TOKEN_SOURCE = `\\d+[A-Z]?` +
158
158
  // followed by a `Phase ` label) or a bare `Phase ` label; a bare number is NOT
159
159
  // a phase-heading intro. The `[^\]]{1,200}` bound mirrors the existing
160
160
  // roadmap-parser heading regexes (ReDoS-safe: a header is one short line).
161
+ //
162
+ // Retained as PR-1 shipped it. PR-2 does not consume it — see the gated
163
+ // selector below, which supersedes it — and it is left byte-identical so an
164
+ // already-merged epic export does not change its accepted language.
161
165
  const PHASE_HEADING_PREFIX_SRC = '(?:\\[[^\\]]{1,200}\\]\\s*(?:Phase\\s+)?|Phase\\s+)';
166
+ // ── #612 PR-2: the ONE bracket identity grammar ─────────────────────────────
167
+ // A bracket phase-ID prefix is `{CODE}.{MM}`. Before this, three spellings of
168
+ // that shape lived in this file and disagreed: extractPhaseToken's bracket
169
+ // branch (`\d+`, case-sensitive), bracketQualifiedKey (`\d+`, mixed-case), and
170
+ // the dir prefix (`\d{2,}`). `GSD.2-05-feature` was simultaneously "not a phase
171
+ // directory" and "phase 05", depending on which one a caller reached. They are
172
+ // now one source.
173
+ //
174
+ // The milestone width mirrors the EMIT grammar rather than accepting any digit
175
+ // run: pad2() emits at least two digits, so two digits — or three-plus with no
176
+ // leading zero — is what toDir can produce. A bare `0` is NOT admitted: the
177
+ // padded `00` is the backlog sentinel's canonical identity and `\d{2}` already
178
+ // covers it, so nothing needs the unpadded spelling. (An earlier revision of
179
+ // this comment claimed the opposite; the constant below has always rejected it
180
+ // — #2867 review, Minor 2.) `[GSD.2] 05:` is therefore NOT a bracket id —
181
+ // which is the point: it is the shape that made the three spellings disagree.
182
+ // Reconciled with BRACKET_CANONICAL_NUMERIC_SOURCE above — the width toDir
183
+ // actually emits (pad2: 2 digits, or 3+ with no leading zero). The earlier
184
+ // `(?:\d{2,}|0)` diverged from it in both directions: it admitted `002`, which
185
+ // the emit validator rejects, and a bare `0` that pad2 never produces. Every
186
+ // bracket-milestone recognizer now derives from this one constant — the section
187
+ // recognizers previously spelled `0*N` or `\d+` and accepted `[GSD.2]`, which
188
+ // SCOPED a milestone no phase heading could then resolve into, recreating the
189
+ // on-disk-count fallback this PR exists to remove.
190
+ //
191
+ // An unpadded bracket is therefore MALFORMED, uniformly: it scopes nothing,
192
+ // bounds nothing, sections nothing, and is not a phase id. W005 on its
193
+ // directories is the signal that surfaces it.
194
+ const BRACKET_MILESTONE_NUMERIC_SRC = BRACKET_CANONICAL_NUMERIC_SOURCE;
195
+ // #2761 M3 (trek-e review): the bracket PROJECT-CODE class, as a named source
196
+ // rather than a class this file (and three readers outside it) each re-typed.
197
+ // The re-typed copies were the exact drift #2761's own gate forbids — "no token
198
+ // literal outside src/phase-id.cts" — and `check:phase-id-drift` did not see
199
+ // them, because its detector only knew the phase-NUMBER token grammar. The
200
+ // guard now carries a bracket rule too (scripts/lint-phase-id-drift.cjs).
201
+ const BRACKET_PROJECT_CODE_SRC = '[A-Z][A-Z0-9_]*';
202
+ const BRACKET_ID_SRC = `${BRACKET_PROJECT_CODE_SRC}\\.${BRACKET_MILESTONE_NUMERIC_SRC}`;
203
+ // The bracket MILESTONE INTRO — `[CODE.MM]` — in the two shapes its readers
204
+ // need. Both were re-typed verbatim outside this module before #2761 M3:
205
+ //
206
+ // * PINNED, to one already-resolved milestone integer. This owns the pad2
207
+ // spelling rule as well as the grammar, so "canonical spelling only, not
208
+ // `0*N`" (the rule that keeps `[GSD.2]` from scoping a milestone no phase
209
+ // heading can resolve into) lives in ONE place instead of being restated
210
+ // beside every regex. `roadmap-parser`'s bracket-fallback selector and
211
+ // `state`'s `isMilestoneBounded` both consume this.
212
+ //
213
+ // * CAPTURING, over any milestone, putting the milestone digits in a group.
214
+ // `validate`'s `checkBracketCoherence` consumes this.
215
+ //
216
+ // The milestone argument is expected to be a safe integer — every caller
217
+ // resolves it through `Number.isSafeInteger` first. A non-integer yields a
218
+ // regex source that is still well-formed and simply matches nothing, which is
219
+ // the same safe degrade the callers' own guards produce.
220
+ function bracketMilestoneIntroSrcFor(milestone) {
221
+ return `\\[${BRACKET_PROJECT_CODE_SRC}\\.${String(milestone).padStart(2, '0')}\\]`;
222
+ }
223
+ const BRACKET_MILESTONE_INTRO_CAPTURING_SRC = `\\[${BRACKET_PROJECT_CODE_SRC}\\.(${BRACKET_MILESTONE_NUMERIC_SRC})\\]`;
224
+ // Recognition is case-INSENSITIVE (every reader compiles `/i`), but the identity
225
+ // helpers this file owns — isSentinelPhaseId, getMilestoneFromPhaseId,
226
+ // bracketQualifiedKey — match `[A-Z]` case-SENSITIVELY. A lowercase bracket id
227
+ // captured by a reader and handed straight to them silently fails every identity
228
+ // test, which is how `### [gsd.999] 07:` leaked into phase counts as a real
229
+ // phase. Fold before any identity operation; never fold for display.
230
+ function foldBracketId(bracketId) {
231
+ return String(bracketId).toUpperCase();
232
+ }
233
+ // The identity recognizers every bracket helper in this file shares.
234
+ // `BRACKET_ID_PREFIX_RE` is applied to an ALREADY-FOLDED string, so it needs no
235
+ // `/i`; the other two see raw dir names and carry it.
236
+ // The milestone field must END at the phase separator. Without the boundary the
237
+ // width alternation matched a PREFIX of a malformed run — `GSD.002-01` matched
238
+ // its leading `00` and read as a sentinel.
239
+ const BRACKET_ID_PREFIX_RE = new RegExp(`^${BRACKET_PROJECT_CODE_SRC}\\.(${BRACKET_MILESTONE_NUMERIC_SRC})(?=-|$)`);
240
+ const BRACKET_DIR_PREFIX_SRC = `${BRACKET_ID_SRC}-`;
241
+ // The trailing `(?=-|$)` is what makes the recognizer and the resolver agree on
242
+ // REJECTED input, not just accepted input. Without it `GSD.02-12A-hotfix`
243
+ // resolves to token `12` here while the directory recognizer calls the name
244
+ // malformed — so W005 reports it malformed in the same run that the
245
+ // milestone-complete check treats it as a real phase directory.
246
+ const BRACKET_DIR_TOKEN_RE = new RegExp(`^${BRACKET_DIR_PREFIX_SRC}(\\d+(?:\\.\\d+)?)(?=-|$)`, 'i');
247
+ // Same width rule, same `(?=-|$)` boundary and same single-sub-phase shape as
248
+ // BRACKET_DIR_TOKEN_RE. Without them a qualified query `GSD.02-12` matched the
249
+ // directory `GSD.02-12A-hotfix` — which isPhaseDirName calls malformed — and
250
+ // phaseTokenMatches returns UNCONDITIONALLY on a qualified hit, so that
251
+ // disagreement would have been final rather than a fall-through.
252
+ const BRACKET_QUALIFIED_KEY_RE = new RegExp(`^(${BRACKET_PROJECT_CODE_SRC})\\.(${BRACKET_MILESTONE_NUMERIC_SRC})-(\\d+(?:\\.\\d+)?)(?=-|$)`, 'i');
253
+ // ── #612 PR-2: gated heading-intro selection ────────────────────────────────
254
+ // The two intro spellings that exist upstream TODAY, transcribed verbatim from
255
+ // the call sites. A repo that has not opted into the bracket convention
256
+ // compiles exactly these — not a superset of them, THEM — so its reads are
257
+ // structurally identical to the base build rather than argued equivalent.
258
+ // tests/adr-612-bracket-heading-selection.test.cjs asserts that byte-equality
259
+ // against its own independently transcribed copies of the call-site literals.
260
+ const BASE_ANY_BRACKET_HEADING_PREFIX_SRC = '(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+';
261
+ const BASE_PHASE_LABEL_PREFIX_SRC = 'Phase\\s+';
262
+ // Which of those two a site spells at base. Passed explicitly rather than
263
+ // inferred, because the choice is a fact about the call site's history that no
264
+ // amount of looking at the widened pattern can recover.
265
+ const PHASE_HEADING_BASELINE = Object.freeze({
266
+ /** Site already tolerates `[anything] Phase N` — roadmap headings, validate's heading scanner. */
267
+ ANY_BRACKET: 'any-bracket',
268
+ /** Site spells a bare `Phase N` with no bracket tolerance — checklist bullets, the counters. */
269
+ LABEL_ONLY: 'label-only',
270
+ });
271
+ /**
272
+ * The heading-intro source a site should compile, given the resolved
273
+ * `phase_id_convention`.
274
+ *
275
+ * NON-bracket conventions (null, undefined, 'milestone-prefixed', or any
276
+ * unrecognized value) return the site's BASE spelling unchanged. This is the
277
+ * whole design: PR-2 originally widened these reads ungated and argued the new
278
+ * shape "cannot occur in a legacy ROADMAP", which is false — `### [RFC.2119] 5:`,
279
+ * `### [v1.0] 2024:` and `### [ADR.612] 3:` are all legal legacy headings that
280
+ * the widened form claims as phases, moving phase_count, total_phases and W006
281
+ * on repos that never opted in. Selection at construction time removes the
282
+ * argument entirely: there is nothing to reason about, because a non-bracket
283
+ * repo compiles the same source string it compiled before.
284
+ *
285
+ * `capturing` adds EXACTLY ONE group, at position 1, holding the bracket id —
286
+ * `undefined` whenever a non-bracket alternative matched. Sites that filter
287
+ * sentinels need it: READING-B puts the sentinel milestone in the bracket, so
288
+ * testing the phase token alone is blind to `### [GSD.999] 01:`.
289
+ *
290
+ * Pure: takes the resolved convention, never reads config.
291
+ */
292
+ function phaseHeadingPrefixSrcFor(baseline, convention, capturing = false) {
293
+ const base = baseline === PHASE_HEADING_BASELINE.ANY_BRACKET
294
+ ? BASE_ANY_BRACKET_HEADING_PREFIX_SRC
295
+ : BASE_PHASE_LABEL_PREFIX_SRC;
296
+ if (convention !== 'bracket')
297
+ return base;
298
+ const id = capturing ? `(${BRACKET_ID_SRC})` : BRACKET_ID_SRC;
299
+ // `[ \t]*` not `\s*`: `\s` spans newlines, so a bracket-terminated heading
300
+ // followed by a blank line and a digit-leading prose line read as one phase.
301
+ // BOTH bracket forms are admitted at both baselines, and both CAPTURE. The
302
+ // any-bracket base already matches `[GSD.999] Phase 07:` on its own — but
303
+ // through the base alternative, which captures nothing, so the reader saw
304
+ // `bracketId === undefined`, fell back to the legacy leading-integer rule, and
305
+ // counted a labeled icebox heading as a real phase while the label-less form
306
+ // beside it was excluded. Two derivations of one ROADMAP disagreed. The
307
+ // bracket alternative is tried FIRST so it wins the capture.
308
+ const bracketAlt = `\\[${id}\\][ \\t]*(?:Phase\\s+|(?=\\d))`;
309
+ return `(?:${bracketAlt}|${base})`;
310
+ }
162
311
  function stripProjectCodePrefix(value, caseInsensitive = true) {
163
312
  const input = String(value);
164
313
  const re = caseInsensitive ? PROJECT_CODE_PREFIX_STRIP_RE_I : PROJECT_CODE_PREFIX_STRIP_RE;
@@ -200,10 +349,10 @@ function getMilestoneFromPhaseId(phaseId, convention) {
200
349
  // pure (no config read) and backward-compatible: every existing single-arg
201
350
  // caller resolves to the unchanged READING-A body.
202
351
  if (convention === 'bracket') {
203
- const b = String(phaseId).match(/^([A-Z][A-Z0-9_]*)\.(\d+)/);
352
+ const b = foldBracketId(phaseId).match(BRACKET_ID_PREFIX_RE);
204
353
  if (!b)
205
354
  return null;
206
- const mm = parseInt(b[2], 10);
355
+ const mm = parseInt(b[1], 10);
207
356
  if (SENTINEL_RANGES.includes(mm))
208
357
  return null; // sentinel milestones have no real milestone
209
358
  return `v${mm}.0`;
@@ -383,9 +532,36 @@ function isSentinelPhaseId(phaseId, convention) {
383
532
  // convention-less caller uses the legacy/bare leading-int rule below, so no
384
533
  // existing reader gains a false positive; the bracket reading is opt-in.
385
534
  if (convention === 'bracket') {
386
- const bracket = s.match(/^[A-Z][A-Z0-9_]*\.(\d+)/); // bracket: milestone in the prefix
535
+ // #612 PR-2: fold before matching. Readers recognize headings under `/i`, so
536
+ // a lowercase `[gsd.999]` arrives verbatim; the case-sensitive class below
537
+ // then failed to match and an icebox item counted as a real phase.
538
+ const bracket = foldBracketId(s).match(BRACKET_ID_PREFIX_RE);
387
539
  if (bracket)
388
540
  return SENTINEL_RANGES.includes(parseInt(bracket[1], 10));
541
+ // #2761 round-12: NO bracket tag matched — a bare/untagged id under
542
+ // bracket convention (`0-bootstrap`, a directory with no `{CODE}.{MM}-`
543
+ // prefix, or a legacy-spelled `### Phase 0:` heading routed here without
544
+ // its own bracketId guard). Falling through to the LEGACY leading-int rule
545
+ // below would read this bare `0` as sentinel milestone 0 — but under
546
+ // bracket convention milestone 0 is expressed ONLY via an explicit
547
+ // bracket tag, so an untagged leading `0` is a real phase token, not a
548
+ // sentinel. This mirrors the two HEADING-side counters that already carry
549
+ // this exact carve-out: state.cts's `countRoadmapPhaseHeadings` guards its
550
+ // bare-0 exclusion with `bracketId &&` (so an untagged `Phase 0:` heading
551
+ // is counted), and roadmap-parser.cts's `scanMilestonePhaseIds` composes
552
+ // the bare-token rule as 999-only, deliberately NOT this predicate, for
553
+ // the identical reason (#3185: the leading-int rule also swallows the
554
+ // #2554 decimal ids — `0.5-bootstrap` is a real phase, not milestone 0).
555
+ // The 999/icebox reading stays universal either way — an untagged `999`
556
+ // is still backlog under every convention — so only that half of
557
+ // SENTINEL_RANGES applies here.
558
+ //
559
+ // `isSentinelPhaseDir` remains deliberately convention-blind for its
560
+ // warning-only disk guards and may conservatively classify a bare `0`
561
+ // directory as sentinel. This branch has an explicit resolved convention
562
+ // and feeds counts/archives, so it must retain the more precise reading.
563
+ const bare = stripProjectCodePrefix(s).match(/^0*(\d+)/);
564
+ return bare !== null && parseInt(bare[1], 10) === 999;
389
565
  }
390
566
  const legacy = stripProjectCodePrefix(s).match(/^0*(\d+)/); // legacy/bare: leading int
391
567
  if (!legacy)
@@ -599,7 +775,10 @@ function extractPhaseToken(dirName, convention) {
599
775
  // config read). The captured token is dot-only (`PP[.SS]`); the milestone↔phase
600
776
  // hyphen and any trailing plan/slug are excluded.
601
777
  if (convention === 'bracket') {
602
- const bracketDir = dirName.match(/^[A-Z][A-Z0-9_]*\.\d+-(\d+(?:\.\d+)?)/);
778
+ // #612 PR-2: built from the ONE bracket identity grammar, not a private
779
+ // spelling. Case-insensitive to match how the readers recognize headings and
780
+ // directories; the milestone width is the emit grammar's.
781
+ const bracketDir = dirName.match(BRACKET_DIR_TOKEN_RE);
603
782
  if (bracketDir)
604
783
  return bracketDir[1];
605
784
  }
@@ -860,10 +1039,52 @@ function isPhaseArtifact(fileName, phaseDirName) {
860
1039
  function scopeToPhase(fileNames, phaseDirName) {
861
1040
  return fileNames.filter((f) => isPhaseArtifact(f, phaseDirName));
862
1041
  }
1042
+ /**
1043
+ * Canonical comparable key for a milestone-qualified bracket id or dir name.
1044
+ * Lifts the milestone out of the `{CODE}.{MM}-` prefix so a flat multi-milestone
1045
+ * layout disambiguates: `CK.03-02` resolves to its OWN milestone's directory,
1046
+ * never the first same-numbered directory of another milestone.
1047
+ *
1048
+ * Returns null for UNQUALIFIED ids (`02`, `HQ-11`, `11.01`) so callers fall back
1049
+ * to bare-token matching unchanged, and GATED on convention === 'bracket' for
1050
+ * the same reason as extractPhaseToken: the qualified key is padding-
1051
+ * INSENSITIVE where the legacy token path is padding-SENSITIVE, so ungated it
1052
+ * silently widens matching on legacy repos.
1053
+ */
1054
+ function bracketQualifiedKey(s, convention) {
1055
+ if (convention !== 'bracket')
1056
+ return null;
1057
+ const m = String(s).match(BRACKET_QUALIFIED_KEY_RE);
1058
+ if (!m)
1059
+ return null;
1060
+ const milestone = parseInt(m[2], 10);
1061
+ // A milestone integer past Number's exact range collapses to Infinity, and
1062
+ // every such id would then share one key. Refuse rather than collide.
1063
+ if (!Number.isSafeInteger(milestone))
1064
+ return null;
1065
+ const phase = m[3].split('.').map(n => parseInt(n, 10));
1066
+ if (phase.some(n => !Number.isSafeInteger(n)))
1067
+ return null;
1068
+ return `${foldBracketId(m[1])}.${milestone}-${phase.join('.')}`;
1069
+ }
863
1070
  /**
864
1071
  * Check if a directory name's phase token matches the normalized phase exactly.
1072
+ *
1073
+ * The optional `convention` is the ADR-2121 additive shape: every existing
1074
+ * two-argument call site resolves to the unchanged legacy body.
865
1075
  */
866
- function phaseTokenMatches(dirName, normalized) {
1076
+ function phaseTokenMatches(dirName, normalized, convention) {
1077
+ if (convention === 'bracket') {
1078
+ // A milestone-qualified query compares on the full qualified key, and
1079
+ // returns unconditionally: falling through on a miss would re-admit the
1080
+ // cross-milestone match the qualification exists to prevent.
1081
+ const qKey = bracketQualifiedKey(normalized, convention);
1082
+ if (qKey)
1083
+ return bracketQualifiedKey(dirName, convention) === qKey;
1084
+ const bracketToken = extractPhaseToken(dirName, convention);
1085
+ if (bracketToken.toUpperCase() === normalized.toUpperCase())
1086
+ return true;
1087
+ }
867
1088
  const token = extractPhaseToken(dirName);
868
1089
  if (token.toUpperCase() === normalized.toUpperCase())
869
1090
  return true;
@@ -960,8 +1181,8 @@ const unpad = (digits) => digits.replace(/^0+(?=\d)/, '');
960
1181
  * the directory's leading digit run instead of `extractPhaseToken` (whose
961
1182
  * token for these dirs is the mis-absorbed multi-segment form).
962
1183
  */
963
- function matchPhaseDirs(dirs, normalized) {
964
- const primary = dirs.filter(d => phaseTokenMatches(d, normalized));
1184
+ function matchPhaseDirs(dirs, normalized, convention) {
1185
+ const primary = dirs.filter(d => phaseTokenMatches(d, normalized, convention));
965
1186
  if (primary.length > 0)
966
1187
  return { matches: primary, usedBareFallback: false };
967
1188
  const bare = String(normalized);
@@ -1021,9 +1242,18 @@ function phaseKeyFromToken(token) {
1021
1242
  /**
1022
1243
  * Canonical key for a phase DIRECTORY name (`"05-schedule-8"` → `"05"`,
1023
1244
  * `"PROJ-5-x"` → `"05"`, `"30.1-follow-up"` → `"30.1"`).
1245
+ *
1246
+ * #612: `convention` is forwarded to `extractPhaseToken`, which needs that signal
1247
+ * to read a bracket directory (`"GSD.02-05-delta"` → `"05"`) — a bracket dir is
1248
+ * string-indistinguishable from the legacy letter-prefixed-decimal family, so the
1249
+ * extractor refuses to guess. Optional and defaulted-absent, so every pre-#612
1250
+ * call site resolves byte-identically to prior behaviour. Without it a bracket dir
1251
+ * yields its whole name as the key and never matches the ROADMAP entry it names —
1252
+ * #2562's own defect class reached from the other side: both sides of a comparison
1253
+ * must be derived not merely by the same function but under the same convention.
1024
1254
  */
1025
- function phaseKeyFromDir(dirName) {
1026
- return phaseKeyFromToken(extractPhaseToken(dirName));
1255
+ function phaseKeyFromDir(dirName, convention) {
1256
+ return phaseKeyFromToken(extractPhaseToken(dirName, convention));
1027
1257
  }
1028
1258
  /**
1029
1259
  * Canonical key for a phase referenced in PROSE — a ROADMAP `## Progress` table
@@ -1176,6 +1406,18 @@ module.exports = {
1176
1406
  isPhaseContinuationSegment,
1177
1407
  BRACKET_PHASE_TOKEN_SOURCE,
1178
1408
  PHASE_HEADING_PREFIX_SRC,
1409
+ BRACKET_ID_SRC,
1410
+ BRACKET_PROJECT_CODE_SRC,
1411
+ bracketMilestoneIntroSrcFor,
1412
+ BRACKET_MILESTONE_INTRO_CAPTURING_SRC,
1413
+ BRACKET_MILESTONE_NUMERIC_SRC,
1414
+ BRACKET_DIR_PREFIX_SRC,
1415
+ BASE_ANY_BRACKET_HEADING_PREFIX_SRC,
1416
+ BASE_PHASE_LABEL_PREFIX_SRC,
1417
+ PHASE_HEADING_BASELINE,
1418
+ phaseHeadingPrefixSrcFor,
1419
+ foldBracketId,
1420
+ bracketQualifiedKey,
1179
1421
  stripProjectCodePrefix,
1180
1422
  normalizePhaseName,
1181
1423
  getMilestoneFromPhaseId,