@opengsd/gsd-core 1.10.0 → 1.11.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 (328) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-debug-session-manager.md +11 -0
  4. package/agents/gsd-doc-synthesizer.md +2 -4
  5. package/agents/gsd-executor.md +5 -5
  6. package/agents/gsd-mempalace-curator.md +5 -2
  7. package/agents/gsd-phase-researcher.md +20 -1
  8. package/agents/gsd-plan-checker.md +37 -0
  9. package/agents/gsd-planner.md +44 -46
  10. package/agents/gsd-user-profiler.md +3 -0
  11. package/agents/gsd-verifier.md +12 -3
  12. package/bin/install.js +841 -971
  13. package/bin/lib/ui-safety-gate.cjs +2 -0
  14. package/commands/gsd/code-review.md +1 -1
  15. package/commands/gsd/execute-phase.md +1 -1
  16. package/commands/gsd/map-codebase.md +1 -1
  17. package/commands/gsd/mempalace-capture.md +1 -1
  18. package/commands/gsd/mempalace-recall.md +1 -1
  19. package/commands/gsd/new-milestone.md +1 -1
  20. package/commands/gsd/quick.md +1 -1
  21. package/commands/gsd/review-backlog.md +2 -1
  22. package/commands/gsd/verify-work.md +1 -1
  23. package/gsd-core/bin/gsd-tools.cjs +469 -88
  24. package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
  25. package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
  26. package/gsd-core/bin/lib/api-coverage.cjs +3 -5
  27. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  28. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  29. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  30. package/gsd-core/bin/lib/audit.cjs +876 -240
  31. package/gsd-core/bin/lib/broken-windows.cjs +1 -1
  32. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  33. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  34. package/gsd-core/bin/lib/capability-registry.cjs +575 -101
  35. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  36. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  37. package/gsd-core/bin/lib/capability-validator.cjs +495 -22
  38. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  39. package/gsd-core/bin/lib/check-command-router.cjs +71 -37
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  41. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  43. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  44. package/gsd-core/bin/lib/commands.cjs +651 -86
  45. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  47. package/gsd-core/bin/lib/config-loader.cjs +75 -0
  48. package/gsd-core/bin/lib/config.cjs +10 -1
  49. package/gsd-core/bin/lib/core-utils.cjs +127 -29
  50. package/gsd-core/bin/lib/decisions.cjs +23 -0
  51. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  52. package/gsd-core/bin/lib/frontmatter.cjs +155 -20
  53. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  54. package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
  55. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  56. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  57. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  58. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  59. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  60. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  61. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  62. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  63. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  64. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  65. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  66. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  67. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  68. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  69. package/gsd-core/bin/lib/init.cjs +321 -129
  70. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  71. package/gsd-core/bin/lib/install-engine.cjs +745 -258
  72. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  73. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  74. package/gsd-core/bin/lib/install-profiles.cjs +134 -57
  75. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  76. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  77. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  78. package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
  79. package/gsd-core/bin/lib/io.cjs +10 -0
  80. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  81. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  82. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  83. package/gsd-core/bin/lib/milestone.cjs +754 -70
  84. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  85. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  86. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  87. package/gsd-core/bin/lib/pattern.cjs +122 -0
  88. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  89. package/gsd-core/bin/lib/phase-id.cjs +444 -36
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  91. package/gsd-core/bin/lib/phase-locator.cjs +125 -18
  92. package/gsd-core/bin/lib/phase.cjs +646 -143
  93. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  94. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  95. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  96. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  97. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  98. package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
  99. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  100. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  101. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  102. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  103. package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
  104. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  105. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  106. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  107. package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
  108. package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
  109. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  110. package/gsd-core/bin/lib/roadmap.cjs +385 -94
  111. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
  112. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  113. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
  114. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  115. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  116. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
  117. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  118. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  119. package/gsd-core/bin/lib/security.cjs +104 -5
  120. package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
  121. package/gsd-core/bin/lib/smart-entry.cjs +142 -22
  122. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  123. package/gsd-core/bin/lib/state-document.cjs +152 -8
  124. package/gsd-core/bin/lib/state-transition.cjs +371 -117
  125. package/gsd-core/bin/lib/state.cjs +1794 -357
  126. package/gsd-core/bin/lib/surface.cjs +23 -9
  127. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  128. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  129. package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
  130. package/gsd-core/bin/lib/uat.cjs +399 -56
  131. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  132. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  133. package/gsd-core/bin/lib/unusable-input.cjs +24 -0
  134. package/gsd-core/bin/lib/update-context.cjs +8 -2
  135. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  136. package/gsd-core/bin/lib/validate.cjs +20 -6
  137. package/gsd-core/bin/lib/vendor/README.md +37 -0
  138. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  139. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  140. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  141. package/gsd-core/bin/lib/verification.cjs +258 -8
  142. package/gsd-core/bin/lib/verify.cjs +368 -888
  143. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  144. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  145. package/gsd-core/bin/lib/workstream.cjs +2 -2
  146. package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
  147. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  148. package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
  149. package/gsd-core/references/agent-contracts.md +43 -26
  150. package/gsd-core/references/checkpoints.md +2 -2
  151. package/gsd-core/references/context-budget.md +1 -1
  152. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  153. package/gsd-core/references/doc-conflict-engine.md +1 -1
  154. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  155. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  156. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  157. package/gsd-core/references/execute-phase-response-language.md +1 -1
  158. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  159. package/gsd-core/references/gate-prompts.md +1 -1
  160. package/gsd-core/references/git-planning-commit.md +2 -1
  161. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  162. package/gsd-core/references/model-profiles.md +12 -4
  163. package/gsd-core/references/mvp-concepts.md +9 -9
  164. package/gsd-core/references/planner-guidance.md +3 -9
  165. package/gsd-core/references/planner-preconditions.md +1 -1
  166. package/gsd-core/references/planner-reviews.md +1 -1
  167. package/gsd-core/references/planning-config.md +8 -6
  168. package/gsd-core/references/revision-loop.md +1 -1
  169. package/gsd-core/references/specless-probe-fallback.md +1 -1
  170. package/gsd-core/references/universal-anti-patterns.md +3 -3
  171. package/gsd-core/references/verifier-phase-gates.md +192 -0
  172. package/gsd-core/references/verify-mvp-mode.md +1 -1
  173. package/gsd-core/references/workstream-flag.md +22 -6
  174. package/gsd-core/templates/discussion-log.md +1 -1
  175. package/gsd-core/templates/phase-prompt.md +2 -4
  176. package/gsd-core/templates/state.md +4 -4
  177. package/gsd-core/templates/verification-report.md +9 -1
  178. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  179. package/gsd-core/workflows/autonomous.md +1 -1
  180. package/gsd-core/workflows/cleanup.md +62 -3
  181. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
  182. package/gsd-core/workflows/code-review-fix.md +37 -10
  183. package/gsd-core/workflows/code-review.md +38 -12
  184. package/gsd-core/workflows/complete-milestone.md +141 -18
  185. package/gsd-core/workflows/debug.md +7 -5
  186. package/gsd-core/workflows/diagnose-issues.md +35 -9
  187. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  188. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  189. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
  190. package/gsd-core/workflows/edit-phase.md +26 -1
  191. package/gsd-core/workflows/eval-review.md +3 -5
  192. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
  193. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  194. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
  195. package/gsd-core/workflows/execute-phase.md +38 -50
  196. package/gsd-core/workflows/execute-plan.md +36 -4
  197. package/gsd-core/workflows/explore.md +131 -4
  198. package/gsd-core/workflows/fast.md +10 -2
  199. package/gsd-core/workflows/health.md +73 -4
  200. package/gsd-core/workflows/import.md +4 -4
  201. package/gsd-core/workflows/ingest-docs.md +5 -5
  202. package/gsd-core/workflows/mvp-phase.md +6 -3
  203. package/gsd-core/workflows/new-milestone.md +14 -9
  204. package/gsd-core/workflows/new-project.md +14 -14
  205. package/gsd-core/workflows/next.md +12 -0
  206. package/gsd-core/workflows/plan-phase.md +41 -17
  207. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  208. package/gsd-core/workflows/progress.md +34 -6
  209. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
  210. package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
  211. package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
  212. package/gsd-core/workflows/quick.md +35 -15
  213. package/gsd-core/workflows/review.md +26 -5
  214. package/gsd-core/workflows/secure-phase.md +1 -1
  215. package/gsd-core/workflows/session-report.md +2 -1
  216. package/gsd-core/workflows/settings.md +66 -2
  217. package/gsd-core/workflows/ship.md +104 -44
  218. package/gsd-core/workflows/spec-phase.md +30 -12
  219. package/gsd-core/workflows/sync-skills.md +63 -8
  220. package/gsd-core/workflows/transition.md +46 -11
  221. package/gsd-core/workflows/ui-phase.md +5 -5
  222. package/gsd-core/workflows/ui-review.md +2 -2
  223. package/gsd-core/workflows/update.md +1 -1
  224. package/gsd-core/workflows/validate-phase.md +1 -1
  225. package/gsd-core/workflows/verify-work.md +9 -7
  226. package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
  227. package/hooks/dist/gsd-check-update-worker.js +56 -13
  228. package/hooks/dist/gsd-check-update.js +19 -1
  229. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  230. package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
  231. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  232. package/hooks/dist/gsd-prompt-guard.js +21 -20
  233. package/hooks/dist/gsd-read-injection-scanner.js +38 -24
  234. package/hooks/dist/gsd-statusline.js +18 -0
  235. package/hooks/dist/gsd-update-banner.js +22 -1
  236. package/hooks/dist/gsd-workflow-guard.js +134 -36
  237. package/hooks/dist/lib/git-cmd.js +92 -59
  238. package/hooks/dist/lib/injection-patterns.js +45 -0
  239. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  240. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  241. package/hooks/gsd-agent-isolation-guard.js +103 -14
  242. package/hooks/gsd-check-update-worker.js +56 -13
  243. package/hooks/gsd-check-update.js +19 -1
  244. package/hooks/gsd-cursor-pre-tool.js +0 -3
  245. package/hooks/gsd-cursor-subagent-start.js +77 -2
  246. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  247. package/hooks/gsd-prompt-guard.js +21 -20
  248. package/hooks/gsd-read-injection-scanner.js +38 -24
  249. package/hooks/gsd-statusline.js +18 -0
  250. package/hooks/gsd-update-banner.js +22 -1
  251. package/hooks/gsd-workflow-guard.js +134 -36
  252. package/hooks/lib/git-cmd.js +92 -59
  253. package/hooks/lib/injection-patterns.js +45 -0
  254. package/hooks/lib/isolation-deny-reason.js +39 -0
  255. package/hooks/lib/isolation-sentinel.js +9 -0
  256. package/package.json +21 -9
  257. package/pi/gsd.cjs +19 -5
  258. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  259. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  260. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  261. package/scripts/changeset/lint.cjs +60 -5
  262. package/scripts/check-alias-drift.cjs +7 -43
  263. package/scripts/check-contract-drift.cjs +297 -0
  264. package/scripts/ci-test-scope.cjs +19 -2
  265. package/scripts/command-contract-helpers.cjs +903 -1
  266. package/scripts/gen-adr-index.cjs +728 -38
  267. package/scripts/gen-capability-registry.cjs +3 -15
  268. package/scripts/gen-context-index.cjs +2 -11
  269. package/scripts/gen-health-docs.cjs +390 -0
  270. package/scripts/gen-inventory-manifest.cjs +50 -4
  271. package/scripts/gen-loop-host-contract.cjs +4 -24
  272. package/scripts/gen-registry.cjs +3 -14
  273. package/scripts/lib/alias-drift-families.cjs +46 -0
  274. package/scripts/lib/drift-scan.cjs +278 -0
  275. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  276. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  277. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  278. package/scripts/lint-canary-version-leak.cjs +73 -0
  279. package/scripts/lint-command-contract.cjs +96 -13
  280. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  281. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  282. package/scripts/lint-default-flip-documentation.cjs +193 -0
  283. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  284. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  285. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  286. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  287. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  288. package/scripts/lint-milestone-window-drift.cjs +468 -0
  289. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  290. package/scripts/lint-plan-count-drift.cjs +318 -0
  291. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  292. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  293. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  294. package/scripts/lint-regression-test-names.cjs +15 -13
  295. package/scripts/lint-removed-but-needed.cjs +320 -0
  296. package/scripts/lint-state-field-drift.cjs +805 -0
  297. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  298. package/scripts/lint-test-file-count.allowlist.json +21 -10
  299. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  300. package/scripts/lint-vendored-deps.cjs +124 -0
  301. package/scripts/pr-changed-files.cjs +63 -0
  302. package/scripts/pr-template-policy.cjs +14 -4
  303. package/scripts/prompt-injection-scan.sh +25 -0
  304. package/scripts/require-issue-link-policy.cjs +192 -0
  305. package/scripts/state-write-path-drift-baseline.json +19 -0
  306. package/scripts/sync-runtime-launcher.cjs +2 -4
  307. package/skills/gsd-autonomous/SKILL.md +0 -1
  308. package/skills/gsd-code-review/SKILL.md +1 -1
  309. package/skills/gsd-execute-phase/SKILL.md +1 -2
  310. package/skills/gsd-map-codebase/SKILL.md +1 -1
  311. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  312. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  313. package/skills/gsd-new-milestone/SKILL.md +1 -1
  314. package/skills/gsd-next/SKILL.md +0 -1
  315. package/skills/gsd-plan-phase/SKILL.md +0 -1
  316. package/skills/gsd-progress/SKILL.md +0 -1
  317. package/skills/gsd-quick/SKILL.md +1 -1
  318. package/skills/gsd-review-backlog/SKILL.md +2 -1
  319. package/skills/gsd-stats/SKILL.md +0 -1
  320. package/skills/gsd-verify-work/SKILL.md +1 -1
  321. package/vscode/package.json +1 -1
  322. package/gsd-core/workflows/discovery-phase.md +0 -298
  323. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  324. package/gsd-core/workflows/verify-phase.md +0 -574
  325. package/scripts/affected-tests-lib.cjs +0 -554
  326. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  327. package/scripts/run-affected-tests.cjs +0 -7
  328. package/scripts/run-tests.cjs +0 -1051
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ /**
3
+ * Health Diagnostic — Milestone archive + root hygiene rules (Phase 11,
4
+ * #3309, ADR-3180 §8.2/§8.3/§8.5).
5
+ *
6
+ * Group: "Milestone archive + root hygiene" (design doc, "Rule table
7
+ * organization" table) — W018, W019.
8
+ *
9
+ * Ported behavior-preserving from `cmdValidateHealth`
10
+ * (`src/verify.cts:2301-2354`), the exact call sites for W018/W019.
11
+ *
12
+ * Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
13
+ *
14
+ * ADR-457 build-at-publish: source in
15
+ * src/health-diagnostic-rules/milestone-archive-hygiene.cts, compiled to
16
+ * gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs
17
+ * (gitignored).
18
+ */
19
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
20
+ const healthDiagnosticMod = require("../health-diagnostic-types.cjs");
21
+ const { SEVERITY, REMEDY_ACTION, REMEDY_RISK, adviseRemedy } = healthDiagnosticMod;
22
+ const artifacts_cjs_1 = require("../artifacts.cjs");
23
+ // ─── W018 — MILESTONES.md missing archived milestone(s) (verify.cts:2301-2335) ──
24
+ //
25
+ // Condition: `snapshot.milestoneArchiveStatus.value.archivedVersions` (list of
26
+ // versions with a `.planning/milestones/<ver>-ROADMAP.md` snapshot file) minus
27
+ // `.documentedVersions` (list of `## <version>` headings already present in
28
+ // MILESTONES.md) — versions present in the archive but not documented in the
29
+ // registry. ONE aggregate `Diagnostic` listing every missing version, exactly
30
+ // mirroring the original's single `addIssue` call
31
+ // (`verify.cts:2321-2330`, `` `MILESTONES.md missing ${missingFromRegistry.length}
32
+ // archived milestone(s): ${missingFromRegistry.join(', ')}` ``) — the original
33
+ // computes the FULL list first (`missingFromRegistry`), then fires exactly one
34
+ // `addIssue` after the loop, not once per version. No diagnostic at all if the
35
+ // archive dir has zero recognized `-ROADMAP.md` snapshots (mirrors the
36
+ // original's `if (archivedVersions.length > 0)` guard) or if nothing is
37
+ // missing.
38
+ function checkW018(snapshot) {
39
+ const { archivedVersions, documentedVersions } = snapshot.milestoneArchiveStatus.value;
40
+ if (archivedVersions.length === 0)
41
+ return [];
42
+ const documented = new Set(documentedVersions);
43
+ const missingFromRegistry = archivedVersions.filter((ver) => !documented.has(ver));
44
+ if (missingFromRegistry.length === 0)
45
+ return [];
46
+ return [
47
+ {
48
+ code: 'W018',
49
+ severity: SEVERITY.WARNING,
50
+ message: `MILESTONES.md missing ${missingFromRegistry.length} archived milestone(s): ${missingFromRegistry.join(', ')}`,
51
+ remedy: {
52
+ action: REMEDY_ACTION.BACKFILL_MILESTONES,
53
+ risk: REMEDY_RISK.NONE,
54
+ args: {},
55
+ },
56
+ },
57
+ ];
58
+ }
59
+ // ─── W019 — Unrecognized .planning/ root file (verify.cts:2337-2354) ───────
60
+ //
61
+ // Condition: for each filename in `snapshot.planningRootFiles.value` ending in
62
+ // `.md`, call `isCanonicalPlanningFile(filename)` (bare basename, not a path —
63
+ // `src/artifacts.cts:43`); flag any that return false. One `Diagnostic` PER
64
+ // unrecognized file (array return), mirroring the original's `addIssue` call
65
+ // INSIDE the `for` loop (`verify.cts:2343-2349`) — unlike W018 this is not an
66
+ // aggregate. Fix text copied verbatim from `verify.cts:2347`.
67
+ function checkW019(snapshot) {
68
+ const diagnostics = [];
69
+ for (const filename of snapshot.planningRootFiles.value) {
70
+ if (!filename.endsWith('.md'))
71
+ continue;
72
+ if ((0, artifacts_cjs_1.isCanonicalPlanningFile)(filename))
73
+ continue;
74
+ diagnostics.push({
75
+ code: 'W019',
76
+ severity: SEVERITY.WARNING,
77
+ message: `Unrecognized .planning/ file: ${filename} — not a canonical GSD artifact`,
78
+ remedy: adviseRemedy('Move to .planning/milestones/ archive subdir or delete if stale. See templates/README.md for the canonical artifact list.'),
79
+ });
80
+ }
81
+ return diagnostics;
82
+ }
83
+ // ─── Exports ────────────────────────────────────────────────────────────────
84
+ const RULES = [
85
+ {
86
+ code: 'W018',
87
+ severity: SEVERITY.WARNING,
88
+ description: 'MILESTONES.md missing entry for archived milestone snapshot',
89
+ repairable: true,
90
+ check: checkW018,
91
+ },
92
+ {
93
+ code: 'W019',
94
+ severity: SEVERITY.WARNING,
95
+ description: 'Unrecognized .planning/ root file — not a canonical GSD artifact',
96
+ repairable: false,
97
+ check: checkW019,
98
+ },
99
+ ];
100
+ module.exports = { RULES };
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ /**
3
+ * Health Diagnostic — Phase directory structure rules (Phase 11, #3309,
4
+ * ADR-3180 §8.2/§8.3/§8.5).
5
+ *
6
+ * Group: "Phase directory structure" (design doc, "Rule table organization"
7
+ * table) — W005, W023, I001, W009.
8
+ *
9
+ * Ported behavior-preserving from `cmdValidateHealth`
10
+ * (`src/verify.cts:1893-1990`, the exact call sites for W005/W023/I001/W009),
11
+ * with two disclosed fidelity reductions forced by `PlanningSnapshot`'s
12
+ * current shape (see each rule's own comment below):
13
+ *
14
+ * - I001 cannot name the individual unsummarized PLAN filename (`snapshot.
15
+ * phases.value[i]` exposes only `planCount`/`summaryCount`, not per-plan
16
+ * filenames) — this rule reports a coarser per-PHASE message instead.
17
+ * - W023's original "described" list called `determinePhaseStatus`
18
+ * (`commands.cts:154`), a SIX-way status string ('Not Started'/'Planned'/
19
+ * 'In Progress'/'Executed'/'Needs Review'/'Complete') computed from its own
20
+ * raw `readdirSync` + `*-VERIFICATION.md` frontmatter read of `phaseDir`.
21
+ * This rule cannot re-run that raw read (§8.1 rule 1 forbids ambient I/O in
22
+ * `check`), so `derivePhaseStatusLabel` below reconstructs the same
23
+ * six-way label from fields `PlanningSnapshot` already exposes —
24
+ * `PhaseSnapshot.planCount`/`summaryCount` (the plan/no-plan and
25
+ * in-progress/planned branches, identical inputs to the original) and
26
+ * `PhaseSnapshot.complete`/`verificationStatus` (`isPhaseComplete`'s own
27
+ * §7.4 disk-strict routing of the SAME `*-VERIFICATION.md` file) for the
28
+ * verification-gated branches. This is a disclosed fidelity reduction, not
29
+ * a byte-for-byte guarantee: `verificationStatus` is the DIFFERENT,
30
+ * `readVerificationStatus`-routed vocabulary ('passed'/'gaps_found'/
31
+ * 'human_needed'/'stale'/'unknown'/'missing') and can disagree with a raw
32
+ * frontmatter re-read in edge cases (e.g. a stale-but-frontmatter-"passed"
33
+ * VERIFICATION.md routes to 'stale', not 'passed', under §7.4's staleness
34
+ * handling — this rule reports 'Executed' there, not 'Complete'). No new
35
+ * ambient I/O and no new `PlanningSnapshot` field were needed — every input
36
+ * was already on `PhaseSnapshot`.
37
+ *
38
+ * W009's original message interpolates `${slash('plan-phase')}`
39
+ * (`verify.cts:1982`, ``Re-run ${slash('plan-phase')} with --research to
40
+ * regenerate``), a per-project runtime-resolved value (`formatGsdSlash`,
41
+ * `src/runtime-slash.cts`) this rule's `(snapshot) => Diagnostic[]`
42
+ * signature has no access to. Hardcodes the canonical `/gsd-plan-phase`
43
+ * hyphen form instead, mirroring the sibling "config.json validation"
44
+ * group's W016 rule (`src/health-diagnostic-rules/config-validation.cts`),
45
+ * which hardcodes `/gsd-ai-integration-phase` the same way for the
46
+ * identical reason.
47
+ *
48
+ * Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
49
+ *
50
+ * ADR-457 build-at-publish: source in
51
+ * src/health-diagnostic-rules/phase-structure.cts, compiled to
52
+ * gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs (gitignored).
53
+ */
54
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
55
+ const healthDiagnosticMod = require("../health-diagnostic-types.cjs");
56
+ const { SEVERITY, adviseRemedy } = healthDiagnosticMod;
57
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
58
+ const validateMod = require("../validate.cjs");
59
+ const { phaseDirNameRe } = validateMod;
60
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
61
+ const phaseIdMod = require("../phase-id.cjs");
62
+ const { extractPhaseToken, normalizePhaseName, comparePhaseNum } = phaseIdMod;
63
+ // ─── W005 — phase directory doesn't follow NN-name format (verify.cts:1893-1902) ─
64
+ function checkW005(snapshot) {
65
+ const diagnostics = [];
66
+ for (const name of snapshot.phaseDirs.value) {
67
+ if (!name.match(phaseDirNameRe)) {
68
+ diagnostics.push({
69
+ code: 'W005',
70
+ severity: SEVERITY.WARNING,
71
+ message: `Phase directory "${name}" doesn't follow NN-name format`,
72
+ remedy: adviseRemedy('Rename to match pattern (e.g., 01-setup)'),
73
+ });
74
+ }
75
+ }
76
+ return diagnostics;
77
+ }
78
+ // ─── W023 — phase directories collide on normalized key (verify.cts:1904-1950) ─
79
+ //
80
+ // Groups `snapshot.phaseDirs.value` by `normalizePhaseName(extractPhaseToken(name))`
81
+ // — the exact same two owners (`phase-id.cjs`) the original `verify.cts:1917-1918`
82
+ // call site uses, relocated verbatim rather than reimplemented. Sorted with
83
+ // `comparePhaseNum` + a `localeCompare` tiebreak, mirroring
84
+ // `verify.cts:1930-1932`'s deterministic-output rationale. See the file-level
85
+ // comment for the disclosed "described" fidelity reduction.
86
+ /**
87
+ * Reconstruct `commands.cts:154`'s `determinePhaseStatus` six-way label from
88
+ * fields `PhaseSnapshot` already exposes (no ambient I/O, no new snapshot
89
+ * field — see file-level comment). `complete`/`verificationStatus` come from
90
+ * `isPhaseComplete`'s §7.4 disk-strict routing of the same `*-VERIFICATION.md`
91
+ * file the original raw read targeted.
92
+ */
93
+ function derivePhaseStatusLabel(planCount, summaryCount, complete, verificationStatus) {
94
+ if (planCount === 0)
95
+ return 'Not Started';
96
+ if (summaryCount < planCount && summaryCount > 0)
97
+ return 'In Progress';
98
+ if (summaryCount < planCount)
99
+ return 'Planned';
100
+ // summaryCount >= planCount > 0 — verification-gated, same as the original's
101
+ // post-count fall-through.
102
+ if (complete)
103
+ return 'Complete';
104
+ if (verificationStatus === 'human_needed')
105
+ return 'Needs Review';
106
+ // gaps_found / stale / missing / unknown all land here, same as the
107
+ // original's "verification exists but unrecognized" and "no verification
108
+ // file" branches both returning 'Executed'.
109
+ return 'Executed';
110
+ }
111
+ function checkW023(snapshot) {
112
+ const groups = new Map();
113
+ for (const name of snapshot.phaseDirs.value) {
114
+ const token = extractPhaseToken(name);
115
+ const key = normalizePhaseName(token);
116
+ const list = groups.get(key);
117
+ if (list)
118
+ list.push(name);
119
+ else
120
+ groups.set(key, [name]);
121
+ }
122
+ const phaseByDir = new Map(snapshot.phases.value.map((p) => [p.dir, p]));
123
+ const diagnostics = [];
124
+ for (const [key, dirs] of groups) {
125
+ if (dirs.length < 2)
126
+ continue;
127
+ const described = dirs
128
+ .slice()
129
+ .sort((a, b) => comparePhaseNum(a, b) || String(a).localeCompare(String(b)))
130
+ .map((d) => {
131
+ const phase = phaseByDir.get(d);
132
+ const plans = phase ? phase.planCount : 0;
133
+ const summaries = phase ? phase.summaryCount : 0;
134
+ const complete = phase ? phase.complete : false;
135
+ const verificationStatus = phase ? phase.verificationStatus : 'missing';
136
+ const status = derivePhaseStatusLabel(plans, summaries, complete, verificationStatus);
137
+ return `${d} (${status})`;
138
+ })
139
+ .join(', ');
140
+ diagnostics.push({
141
+ code: 'W023',
142
+ severity: SEVERITY.WARNING,
143
+ message: `Phase directories collide on normalized key "${key}": ${described}`,
144
+ remedy: adviseRemedy('Inspect each directory; rename or remove the duplicate so only one directory maps to this phase key'),
145
+ });
146
+ }
147
+ return diagnostics;
148
+ }
149
+ // ─── I001 — plan(s) without a matching SUMMARY.md (verify.cts:1952-1965) ───
150
+ //
151
+ // GENUINE FIDELITY GAP (see file-level comment): the original is PER-PLAN
152
+ // (`${e.name}/${plan} has no SUMMARY.md`, `plan` an individual PLAN.md
153
+ // filename from `findUnsummarizedPlans`). `PlanningSnapshot`'s
154
+ // `phases.value[i]` carries only `planCount`/`summaryCount` NUMBERS per
155
+ // phase — no per-plan filenames — so this rule cannot name which plan lacks
156
+ // a summary without reading the phase directory directly inside `check`
157
+ // (forbidden by §8.1 rule 1). This rule instead reports one coarser
158
+ // per-PHASE diagnostic naming the deficit count, not the individual
159
+ // filename(s).
160
+ function checkI001(snapshot) {
161
+ const diagnostics = [];
162
+ for (const phase of snapshot.phases.value) {
163
+ const deficit = phase.planCount - phase.summaryCount;
164
+ if (deficit > 0) {
165
+ diagnostics.push({
166
+ code: 'I001',
167
+ severity: SEVERITY.INFO,
168
+ message: `Phase ${phase.dir} has ${deficit} plan(s) without a matching summary`,
169
+ remedy: adviseRemedy('May be in progress'),
170
+ });
171
+ }
172
+ }
173
+ return diagnostics;
174
+ }
175
+ // ─── W009 — Validation Architecture in RESEARCH.md but no VALIDATION.md ────
176
+ // (verify.cts:1967-1990)
177
+ function checkW009(snapshot) {
178
+ const diagnostics = [];
179
+ for (const entry of snapshot.researchValidationStatus.value) {
180
+ if (entry.hasValidationArchitecture && !entry.hasValidationMd) {
181
+ diagnostics.push({
182
+ code: 'W009',
183
+ severity: SEVERITY.WARNING,
184
+ message: `Phase ${entry.dir}: has Validation Architecture in RESEARCH.md but no VALIDATION.md`,
185
+ remedy: adviseRemedy('Re-run /gsd-plan-phase with --research to regenerate'),
186
+ });
187
+ }
188
+ }
189
+ return diagnostics;
190
+ }
191
+ // ─── Exports ────────────────────────────────────────────────────────────────
192
+ const RULES = [
193
+ {
194
+ code: 'W005',
195
+ severity: SEVERITY.WARNING,
196
+ description: 'Phase directory naming mismatch',
197
+ repairable: false,
198
+ check: checkW005,
199
+ },
200
+ {
201
+ code: 'W023',
202
+ severity: SEVERITY.WARNING,
203
+ description: 'Phase directories collide on normalized key',
204
+ repairable: false,
205
+ check: checkW023,
206
+ },
207
+ {
208
+ code: 'I001',
209
+ severity: SEVERITY.INFO,
210
+ description: 'Plan without SUMMARY (may be in progress)',
211
+ repairable: false,
212
+ check: checkI001,
213
+ },
214
+ {
215
+ code: 'W009',
216
+ severity: SEVERITY.WARNING,
217
+ description: 'Phase has Validation Architecture in RESEARCH.md but no VALIDATION.md',
218
+ repairable: false,
219
+ check: checkW009,
220
+ },
221
+ ];
222
+ module.exports = { RULES };
@@ -0,0 +1,265 @@
1
+ "use strict";
2
+ /**
3
+ * Health Diagnostic — ROADMAP/disk consistency rules (Phase 11, #3309,
4
+ * ADR-3180 §8.2/§8.3/§8.5).
5
+ *
6
+ * Group: "ROADMAP/disk consistency" (design doc, "Rule table organization"
7
+ * table) — W006, W007.
8
+ *
9
+ * Ported behavior-preserving from `cmdValidateHealth`
10
+ * (`src/verify.cts:2029-2101`, the exact call sites for W006/W007).
11
+ *
12
+ * Both rules share ONE matcher — `matchPhaseDirs` + `normalizePhaseName`
13
+ * (`src/phase-id.cts`), the same canonical directory-resolution owner
14
+ * `verify.cts:2060/2073` already calls (its own #2528 comment explains why:
15
+ * pairing roadmap phases against disk by intersecting independently-derived
16
+ * TOKEN SETS mislabels digit-leading slugs like `05-80-20-cleanup` in BOTH
17
+ * directions at once — phase 5 reads as missing a directory (W006) AND that
18
+ * directory reads as not in the roadmap (W007) — so both rules here resolve
19
+ * through `matchPhaseDirs`, never a hand-rolled string/token comparison, and
20
+ * `dirsForPhase` below is the single call site both go through, so they
21
+ * cannot independently drift on what "matches" means (#2528's own bug
22
+ * class).
23
+ *
24
+ * DISK-SIDE SOURCE — `allPhaseDirNames`, NOT `phaseDirs` (found while
25
+ * implementing this file, fixed inline rather than deferred).
26
+ * `snapshot.phaseDirs` (Phase 10, `listMilestonePhaseDirs`) is WINDOWED: its
27
+ * `inWindow` filter (`getMilestonePhaseFilter`, `src/roadmap-parser.cts:1220`)
28
+ * admits a directory only when its phase id is a MEMBER of the roadmap's
29
+ * current-milestone-declared phase set (`isDirInMilestone`). That makes
30
+ * `phaseDirs.value` a subset that, by construction, can never contain a
31
+ * directory the roadmap does NOT declare — exactly the directory W007 exists
32
+ * to find. Sourced from `phaseDirs`, W007 would be structurally inert: every
33
+ * member of the set is already provably claimable. Verified empirically
34
+ * (`node -e` trace against a real `buildPlanningSnapshot`): a genuine orphan
35
+ * directory (`04-extra`, no roadmap entry) was silently absent from
36
+ * `phaseDirs.value` and W007 fired zero diagnostics. `phaseDirs`'s windowing
37
+ * also risks a W006 false positive for a phase declared in a NON-current
38
+ * milestone section (`roadmapDeclaredPhases` is built from the FULL raw
39
+ * ROADMAP, all milestones — `src/planning-snapshot.cts:398-436` — while
40
+ * `phaseDirs` is scoped to the current milestone only), so both rules here
41
+ * use the new, additive `allPhaseDirNames` field
42
+ * (`src/planning-snapshot.cts`) instead: every directory actually present
43
+ * under the active `phases/` root, unfiltered by roadmap declaration.
44
+ * Archived-milestone directory names (`verify.cts:2050`,
45
+ * `collectArchivedPhaseDirNames`) are still not exposed as directory NAMES
46
+ * on `PlanningSnapshot`, but the equivalent TOKEN set is: `checkW006` below
47
+ * additionally consults `snapshot.archivedPhaseTokens` (added for the
48
+ * W002/state-consistency group's #3652 fix, reused verbatim here — see that
49
+ * field's own doc comment) so a phase whose only directory lives in a
50
+ * milestone archive (shipped OR the current milestone's own archive layout)
51
+ * no longer reads as W006-missing (Bug 1, found post-migration: the archived
52
+ * fixtures under `tests/milestone-archive.test.cjs` and
53
+ * `tests/verify-health.test.cjs` regressed against the pre-migration
54
+ * `verify.cts` behavior). W007 still never scans archived dirs (its loop is
55
+ * `allPhaseDirNames.value`, the active `phases/` root only), so an archived
56
+ * dir still cannot spuriously read as W007-orphaned either — unchanged.
57
+ *
58
+ * Bug 2 (found alongside Bug 1): `dirsForPhase` below also runs a
59
+ * `phaseVariants()`-based fallback when `matchPhaseDirs` finds nothing — see
60
+ * its own doc comment. Pre-migration, `verify.cts:2071-2073`/`2092-2093` ran
61
+ * this as a SECOND, independent check the migrated matchPhaseDirs-only path
62
+ * had dropped, causing a false W006/W007 whenever ROADMAP and disk spelled
63
+ * the same phase with a different zero-padding (e.g. ROADMAP "01A" vs disk
64
+ * "1A-...").
65
+ *
66
+ * Not-started exclusion (verify.cts:2065/2075-2076,
67
+ * `buildNotStartedPhaseVariants`, `src/validate.cts:160`): the design doc's
68
+ * field table assigns this group only `roadmapDeclaredPhases`/`phaseDirs`,
69
+ * and `roadmapDeclaredPhases` (`src/planning-snapshot.cts:398-436`) does
70
+ * NOT filter not-started phases out — it returns every heading- and
71
+ * checklist-declared phase id regardless of checked state (confirmed by
72
+ * direct read: its `buildRoadmapPhaseVariants` call includes BOTH `[x]` and
73
+ * `[ ]` checklist entries). Omitting the exclusion here would regress a
74
+ * COMMON case: `gsd-core/templates/roadmap.md`'s "Initial Roadmap" shape
75
+ * declares every phase as an unchecked `- [ ] **Phase N: [Name]**` checklist
76
+ * item before any phase directory exists, so a freshly created ROADMAP.md
77
+ * would immediately spam one W006 per phase. `snapshot.roadmapPhaseCheckboxes`
78
+ * (`src/planning-snapshot.cts:457-480`, backs W011 in the STATE.md-consistency
79
+ * group) already parses exactly this `[x]`/`[ ]` state — `check(snapshot)`'s
80
+ * signature grants the full snapshot, not just this group's assigned column,
81
+ * so `isPhaseNotStarted` below reads it directly rather than re-deriving a
82
+ * third independent regex over raw ROADMAP text (forbidden by §8.1 rule 2).
83
+ * KNOWN GAP, disclosed rather than silently dropped: `roadmapPhaseCheckboxes`
84
+ * is keyed by `PHASE_NUMBER_TOKEN_SOURCE` (`phase-id.cts:54`, no dash), so a
85
+ * milestone-dash-prefixed phase id ("2-01") can never match a checkbox key —
86
+ * unlike the original `buildNotStartedPhaseVariants`, which captures the
87
+ * fuller `[\w][\w.-]*` grammar (dashes included). For that id shape only,
88
+ * this rule's not-started exclusion silently no-ops (never excludes), which
89
+ * is the conservative direction (a possible false W006, not a suppressed
90
+ * true one).
91
+ *
92
+ * Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
93
+ *
94
+ * ADR-457 build-at-publish: source in
95
+ * src/health-diagnostic-rules/roadmap-disk-consistency.cts, compiled to
96
+ * gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs
97
+ * (gitignored).
98
+ */
99
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
100
+ const healthDiagnosticMod = require("../health-diagnostic-types.cjs");
101
+ const { SEVERITY, adviseRemedy } = healthDiagnosticMod;
102
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
103
+ const planningScopeMod = require("../planning-scope.cjs");
104
+ const { SCOPE } = planningScopeMod;
105
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
106
+ const phaseIdMod = require("../phase-id.cjs");
107
+ const { matchPhaseDirs, normalizePhaseName, extractPhaseToken, isSentinelPhaseId } = phaseIdMod;
108
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
109
+ const validateMod = require("../validate.cjs");
110
+ const { phaseVariants } = validateMod;
111
+ // ─── Shared matcher — the single call site both W006 and W007 go through ───
112
+ /**
113
+ * Every on-disk directory (from `allPhaseDirNames.value`) that `phaseId`
114
+ * resolves to via the canonical `matchPhaseDirs` selection. Both `checkW006`
115
+ * (does ANY directory resolve) and `computeClaimedDirs` (which directories
116
+ * does the roadmap claim, for W007) call this — one matcher, reused, per the
117
+ * file-level comment.
118
+ */
119
+ function dirsForPhase(dirs, phaseId) {
120
+ const canonical = matchPhaseDirs(dirs, normalizePhaseName(phaseId)).matches;
121
+ if (canonical.length > 0)
122
+ return canonical;
123
+ // Bug 2 (#3309 W006/W007 migration cluster, found while fixing the
124
+ // originally-reported archived-directory gap): `matchPhaseDirs`'s
125
+ // `phaseTokenMatches` compares `extractPhaseToken(dir)` (the directory's
126
+ // LITERAL, un-normalized digit run — e.g. "1A" for `1A-suffix-phase`)
127
+ // against `normalizePhaseName(phaseId)` (which PADS — "01A") case-
128
+ // insensitively, but never unifies a padding/letter-suffix mismatch
129
+ // BETWEEN the two sides: "1A" !== "01A" even though they name the same
130
+ // phase. Pre-migration, `verify.cts:2071-2073`/`2092-2093` ran a SECOND,
131
+ // independent check here — `[...phaseVariants(p)].some((v) =>
132
+ // diskPhases.has(v))` — that the migrated matchPhaseDirs-only path
133
+ // dropped. `phaseVariants` (`validate.cts:101`) is symmetric
134
+ // (padded<->unpadded, letter-suffix preserved both ways), so generating
135
+ // variants from `phaseId` and checking raw-disk-token membership is
136
+ // equivalent to intersecting `phaseVariants(phaseId)` with
137
+ // `phaseVariants(diskToken)` — variants always include their own input
138
+ // verbatim, so this fallback catches exactly the cases `matchPhaseDirs`
139
+ // alone misses without re-deriving a second matcher.
140
+ const variants = phaseVariants(phaseId);
141
+ return dirs.filter((d) => {
142
+ const token = extractPhaseToken(d).toUpperCase();
143
+ for (const variant of variants) {
144
+ if (token === variant.toUpperCase())
145
+ return true;
146
+ }
147
+ return false;
148
+ });
149
+ }
150
+ /**
151
+ * True when `phaseId` has an unchecked (`[ ]`) checklist entry in
152
+ * `roadmapPhaseCheckboxes` under any of its padding/case variants
153
+ * (`phaseVariants`, `src/validate.cts:101` — the same variant-expansion
154
+ * owner `verify.cts:2071/2075` uses for this exact exclusion). See the
155
+ * file-level comment for the KNOWN GAP on dash-shaped ids.
156
+ */
157
+ function isPhaseNotStarted(phaseId, checkboxes) {
158
+ for (const variant of phaseVariants(phaseId)) {
159
+ if (Object.prototype.hasOwnProperty.call(checkboxes, variant) && checkboxes[variant] === false) {
160
+ return true;
161
+ }
162
+ }
163
+ return false;
164
+ }
165
+ // ─── W006 — ROADMAP.md declares a phase with no directory on disk ─────────
166
+ // (verify.cts:2067-2084)
167
+ function checkW006(snapshot) {
168
+ // Mirrors verify.cts:2029's `if (fs.existsSync(roadmapPath))` guard: ROADMAP.md
169
+ // absent or unreadable means the field degrades to `{value: [], scope:
170
+ // UNREADABLE}` (`src/planning-snapshot.cts:401-403/407-409`) and NEITHER
171
+ // W006 nor W007 evaluates — an empty declared-phase list must not be
172
+ // mistaken for "the roadmap legitimately declares zero phases" here.
173
+ if (snapshot.roadmapDeclaredPhases.scope !== SCOPE.COMPLETE)
174
+ return [];
175
+ const dirs = snapshot.allPhaseDirNames.value;
176
+ const archivedTokens = new Set(snapshot.archivedPhaseTokens.value);
177
+ const checkboxes = snapshot.roadmapPhaseCheckboxes.value;
178
+ const diagnostics = [];
179
+ for (const { phaseId } of snapshot.roadmapDeclaredPhases.value) {
180
+ // #3225: sentinel phase ids (999.x/0.x) are never-on-roadmap by
181
+ // convention; a sentinel heading shouldn't demand a directory.
182
+ if (isSentinelPhaseId(phaseId))
183
+ continue;
184
+ if (dirsForPhase(dirs, phaseId).length > 0)
185
+ continue;
186
+ // Bug 1 (#3309 W006/W007 migration cluster): a phase whose ONLY
187
+ // directory lives under a milestone archive
188
+ // (`.planning/milestones/vX.Y-phases/<phase>/`, shipped OR the current
189
+ // milestone's own archive layout) must not read as "no directory on
190
+ // disk" — mirrors `verify.cts:2038`'s
191
+ // `forEachArchivedPhaseToken(planBase, (token) => diskPhases.add(token))`
192
+ // feeding the archived-phase token set into this exact existence check.
193
+ // `snapshot.archivedPhaseTokens` (`src/planning-snapshot.cts`) is the
194
+ // same token set, reused verbatim from the W002/state-consistency
195
+ // group's own fix for the analogous gap — not a re-derivation.
196
+ if ([...phaseVariants(phaseId)].some((v) => archivedTokens.has(v)))
197
+ continue;
198
+ if (isPhaseNotStarted(phaseId, checkboxes))
199
+ continue;
200
+ diagnostics.push({
201
+ code: 'W006',
202
+ severity: SEVERITY.WARNING,
203
+ message: `Phase ${phaseId} in ROADMAP.md but no directory on disk`,
204
+ remedy: adviseRemedy('Create phase directory or remove from roadmap'),
205
+ });
206
+ }
207
+ return diagnostics;
208
+ }
209
+ // ─── W007 — an on-disk phase directory has no matching ROADMAP entry ──────
210
+ // (verify.cts:2086-2101)
211
+ /** Every directory in `dirs` that ANY declared roadmap phase resolves to. */
212
+ function computeClaimedDirs(dirs, declaredPhases) {
213
+ const claimed = new Set();
214
+ for (const { phaseId } of declaredPhases) {
215
+ for (const dir of dirsForPhase(dirs, phaseId))
216
+ claimed.add(dir);
217
+ }
218
+ return claimed;
219
+ }
220
+ function checkW007(snapshot) {
221
+ // Same guard as W006 — see its comment.
222
+ if (snapshot.roadmapDeclaredPhases.scope !== SCOPE.COMPLETE)
223
+ return [];
224
+ const dirs = snapshot.allPhaseDirNames.value;
225
+ const claimedDirs = computeClaimedDirs(dirs, snapshot.roadmapDeclaredPhases.value);
226
+ const diagnostics = [];
227
+ for (const dirName of dirs) {
228
+ // `extractPhaseToken` is the phase-id.cts owner `PHASE_TOKEN_FROM_DIR_RE`
229
+ // (`src/validate.cts:73-76`) is documented to match exactly
230
+ // (verify.cts's original `p` key from `collectDiskPhaseEntries`,
231
+ // `verify.cts:1373-1397`) — same token, relocated read, not reinvented.
232
+ const token = extractPhaseToken(dirName);
233
+ // #3225: a sentinel dir on disk (999-interim, 0-drafts) is defined as
234
+ // never-on-roadmap; it must not trigger W007.
235
+ if (isSentinelPhaseId(token))
236
+ continue;
237
+ if (claimedDirs.has(dirName))
238
+ continue;
239
+ diagnostics.push({
240
+ code: 'W007',
241
+ severity: SEVERITY.WARNING,
242
+ message: `Phase ${token} exists on disk but not in ROADMAP.md`,
243
+ remedy: adviseRemedy('Add to roadmap or remove directory'),
244
+ });
245
+ }
246
+ return diagnostics;
247
+ }
248
+ // ─── Exports ────────────────────────────────────────────────────────────────
249
+ const RULES = [
250
+ {
251
+ code: 'W006',
252
+ severity: SEVERITY.WARNING,
253
+ description: 'Phase in ROADMAP but no directory',
254
+ repairable: false,
255
+ check: checkW006,
256
+ },
257
+ {
258
+ code: 'W007',
259
+ severity: SEVERITY.WARNING,
260
+ description: 'Phase on disk but not in ROADMAP',
261
+ repairable: false,
262
+ check: checkW007,
263
+ },
264
+ ];
265
+ module.exports = { RULES };