@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,398 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Registry-completeness guard for the `.planning/`-root artifact registry
6
+ * (epic #3180, ADR-3180 §8.4 deliverable C, Phase 12 #3310).
7
+ *
8
+ * `src/artifacts.cts`'s `isCanonicalPlanningFile` enumerates every file name
9
+ * gsd workflows officially write at the `.planning/` ROOT (used today by
10
+ * `validate.health`'s W019 to flag unrecognized files). Nothing previously
11
+ * checked the OTHER direction: that every actual writer of a `.planning/`
12
+ * root file is itself represented in that registry. This guard closes that
13
+ * gap statically — it does not run any code, it scans `src/*.cts` for a
14
+ * write whose target file name can be determined AT READ TIME and checks
15
+ * that name against the real (compiled) `isCanonicalPlanningFile`.
16
+ *
17
+ * ## What counts as a checkable write
18
+ *
19
+ * A call to `platformWriteSync(` or `fs.writeFileSync(` whose first argument
20
+ * resolves — through same-file, single-hop static tracing only — to a
21
+ * LITERAL `.md`/`.json` file name joined onto an UNAMBIGUOUS `.planning/`
22
+ * root expression. Three source shapes are recognized, all real patterns
23
+ * found in this codebase (`src/roadmap.cts`, `src/state.cts`,
24
+ * `src/config.cts`, `src/milestone.cts`, `src/health-diagnostic.cts`):
25
+ *
26
+ * 1. `path.join(<ROOT>, 'Literal.md')` — inline, or assigned first to a
27
+ * `const`/`let` binding or an object-literal property
28
+ * (`key: path.join(<ROOT>, 'Literal.md')`) that is later passed to the
29
+ * write call by name. Object-literal properties are traced by NAME
30
+ * only (no cross-function data-flow) — this matches the actual
31
+ * `RepairPaths`-style convention this repo already uses
32
+ * (`src/health-diagnostic.cts:209-218`), where a destructured
33
+ * parameter reuses the same identifier the property was defined with.
34
+ * 2. `planningPaths(cwd).<prop>` for `<prop>` in `state`, `roadmap`,
35
+ * `project`, `config`, `requirements` — the `PlanningPaths` interface's
36
+ * own file-valued properties (`src/planning-workspace.cts`), which are
37
+ * already a full path, not a directory to join further.
38
+ * 3. A `<ROOT>` in both forms above means EXACTLY `planningRoot(cwd)`,
39
+ * `planningDir(cwd)` (no second argument), or `planningPaths(cwd)`
40
+ * (no second argument) `.planning` — deliberately excluding any call
41
+ * that passes a workstream/project argument (`planningDir(cwd, ws)`),
42
+ * because that form can resolve UNDER `.planning/workstreams/<ws>/`
43
+ * instead of the `.planning/` root, and this guard cannot tell
44
+ * statically whether `ws` is truthy at runtime. Per this guard's own
45
+ * design brief: "false negatives are safer than false positives" — an
46
+ * ambiguous root expression is silently skipped, never reported either
47
+ * way.
48
+ *
49
+ * Anything else — a template-literal or otherwise runtime-computed target,
50
+ * a multi-segment join landing under `phases/`, `milestones/`, or
51
+ * `workstreams/`, a path built through an intermediate helper this guard
52
+ * does not recognize (e.g. `path.dirname(x)`, a ternary, a `.gsd/`
53
+ * fallback) — is silently skipped. This guard reports VIOLATIONS only; a
54
+ * skipped write is never counted as a pass either. See the module docblock
55
+ * above `src/artifacts.cts` for the registry's own stated scope
56
+ * (".planning/ root level" only) — this guard shares that scope.
57
+ *
58
+ * ## Caveat: same-file, name-based tracing (not a real data-flow analysis)
59
+ *
60
+ * Both the `const`/`let` and object-literal-property forms are tracked in a
61
+ * single flat, WHOLE-FILE, name -> file-name map (no per-function scoping).
62
+ * If the same identifier were reused in one file for two unrelated purposes
63
+ * — one a real `.planning/`-root join, the other something else entirely —
64
+ * this guard could mis-resolve the second one. No such collision exists in
65
+ * `src/*.cts` today (verified during implementation); this is a known,
66
+ * accepted heuristic limit, matching this guard's explicitly simpler
67
+ * (non-function-scoped) design versus its sibling
68
+ * `lint-planning-snapshot-bypass-drift.cjs`.
69
+ *
70
+ * ## No ratchet / no baseline
71
+ *
72
+ * Unlike its `lint-*-drift.cjs` siblings, this is a bare pass/fail check,
73
+ * not a shrinking-debt baseline: a ground-truth sweep of this codebase
74
+ * found every real `.planning/`-root writer already registered, so there is
75
+ * no inherited debt to grandfather. Any violation this guard reports is a
76
+ * genuine, actionable regression.
77
+ *
78
+ * Tree-walk / root-confinement / symlink / sanitizer machinery is shared
79
+ * via `scripts/lib/drift-scan.cjs`, exactly like every sibling guard.
80
+ */
81
+
82
+ const fs = require('node:fs');
83
+ const path = require('node:path');
84
+ const driftScan = require('./lib/drift-scan.cjs');
85
+ const { sanitizeForReport, scanTree } = driftScan;
86
+
87
+ const REPO_ROOT = path.join(__dirname, '..');
88
+ const COMPILED_MODULE_REL = path.join('gsd-core', 'bin', 'lib', 'artifacts.cjs');
89
+ const COMPILED_MODULE_PATH = path.join(REPO_ROOT, COMPILED_MODULE_REL);
90
+
91
+ // Authored TypeScript source only — mirrors every sibling drift guard.
92
+ const SCAN_DIRS = ['src'];
93
+ const SCAN_EXT = new Set(['.cts']);
94
+
95
+ // `PlanningPaths` (src/planning-workspace.cts) properties that are
96
+ // themselves a FULL FILE path (not a directory) at the `.planning/` root,
97
+ // mapped to the literal file name they resolve to. `planning`/`phases`/
98
+ // `debug` are deliberately absent — those are directories, not files.
99
+ const PLANNING_PATHS_FILE_PROPS = new Map([
100
+ ['state', 'STATE.md'],
101
+ ['roadmap', 'ROADMAP.md'],
102
+ ['project', 'PROJECT.md'],
103
+ ['config', 'config.json'],
104
+ ['requirements', 'REQUIREMENTS.md'],
105
+ ]);
106
+
107
+ // The three unambiguous `.planning/`-root expressions this guard recognizes
108
+ // as the first argument of `path.join(...)`. Each takes ONLY `cwd` — a call
109
+ // carrying a workstream/project argument is excluded (see module docblock).
110
+ const ROOT_CALL_SRC = String.raw`planningRoot\(cwd\)|planningDir\(cwd\)|planningPaths\(cwd\)\.planning`;
111
+
112
+ // A quoted literal file name ending in `.md` or `.json` — single-quoted or
113
+ // double-quoted, as two separate alternatives (groups: single-quoted
114
+ // filename, double-quoted filename) rather than a `(['"])...\1`
115
+ // backreference: this fragment is spliced into several different larger
116
+ // regexes below at different capture-group OFFSETS, so a fixed
117
+ // backreference number (`\1`) would silently point at whichever group
118
+ // happens to be first in THAT particular composed regex, not necessarily
119
+ // this fragment's own quote group. Every call site reads
120
+ // `matched[i] ?? matched[i + 1]` for the two alternative filename groups
121
+ // this fragment always contributes, in order. Deliberately excludes
122
+ // backticks: a template literal is a runtime-computed target by definition
123
+ // and must never match here.
124
+ const LITERAL_FILENAME_SRC = String.raw`(?:'([^'\\]+\.(?:md|json))'|"([^"\\]+\.(?:md|json))")`;
125
+
126
+ // `const X = <ROOT>;` / `let X = <ROOT>;` — binds X to an unambiguous root
127
+ // expression, so a later `path.join(X, 'Literal.md')` can resolve through
128
+ // it (mirrors `src/config.cts`'s `planningBase` / `src/health-diagnostic
129
+ // .cts`'s `rootBase`/`wsBase`).
130
+ const ROOT_VAR_ASSIGN_RE = new RegExp(String.raw`\b(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:${ROOT_CALL_SRC})\s*;`);
131
+
132
+ // A "binding" — either `const X = ...` / `let X = ...`, or an object-literal
133
+ // property `X: ...` — shared by both join-tracing regexes below so a
134
+ // destructured-later property (the `RepairPaths` convention) traces the
135
+ // same way a local variable does. Group 1 is the const/let name, group 2 is
136
+ // the property name; callers use whichever is non-undefined.
137
+ const BINDING_PREFIX_SRC = String.raw`(?:(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=|([A-Za-z_$][\w$]*)\s*:)`;
138
+
139
+ // `<binding> = path.join(<ROOT>, 'Literal.md')` — the root expression is
140
+ // spelled out inline (not through an intermediate variable).
141
+ const JOIN_FROM_ROOT_CALL_RE = new RegExp(
142
+ String.raw`${BINDING_PREFIX_SRC}\s*path\.join\(\s*(?:${ROOT_CALL_SRC})\s*,\s*${LITERAL_FILENAME_SRC}\s*\)`,
143
+ );
144
+
145
+ // `<binding> = path.join(IDENT, 'Literal.md')` — the root expression was
146
+ // already bound to IDENT by ROOT_VAR_ASSIGN_RE elsewhere in the file.
147
+ const JOIN_FROM_IDENT_RE = new RegExp(
148
+ String.raw`${BINDING_PREFIX_SRC}\s*path\.join\(\s*([A-Za-z_$][\w$]*)\s*,\s*${LITERAL_FILENAME_SRC}\s*\)`,
149
+ );
150
+
151
+ // `<binding> = planningPaths(cwd).<prop>` — PLANNING_PATHS_FILE_PROPS below
152
+ // maps <prop> to its file name.
153
+ const PLANNING_PATHS_PROP_RE = new RegExp(
154
+ String.raw`${BINDING_PREFIX_SRC}\s*planningPaths\(cwd\)\.([A-Za-z_$][\w$]*)\b`,
155
+ );
156
+
157
+ // The write calls this guard checks the first argument of.
158
+ const WRITE_CALL_RE = /\b(?:platformWriteSync|fs\.writeFileSync)\(/g;
159
+
160
+ // A bare identifier, or an inline `path.join(<ROOT>, 'Literal.md')` /
161
+ // `planningPaths(cwd).<prop>` expression, as the resolved first-argument
162
+ // text of a write call.
163
+ const INLINE_JOIN_ROOT_RE = new RegExp(String.raw`^path\.join\(\s*(?:${ROOT_CALL_SRC})\s*,\s*${LITERAL_FILENAME_SRC}\s*\)$`);
164
+ const INLINE_PLANNING_PATHS_PROP_RE = /^planningPaths\(cwd\)\.([A-Za-z_$][\w$]*)$/;
165
+ const BARE_IDENT_RE = /^[A-Za-z_$][\w$]*$/;
166
+
167
+ /**
168
+ * Strip `//` line comments and `/* ... *\/` block comments from `line`,
169
+ * preserving the CONTENTS of single/double/backtick-quoted strings verbatim
170
+ * (so a filename literal or an identifier that happens to sit inside a
171
+ * string is never mistaken for code, but a `//`/`/*` inside a string never
172
+ * truncates the line either). No cross-line state: a template literal or
173
+ * block comment that spans multiple lines is left as-is on each line it
174
+ * touches — every real call/assignment this guard matches is single-line in
175
+ * `src/*.cts` today, so cross-line tracking would add complexity with no
176
+ * observed benefit (see this guard's "simpler than its sibling" design
177
+ * note).
178
+ */
179
+ function stripLineComment(line) {
180
+ let out = '';
181
+ let i = 0;
182
+ while (i < line.length) {
183
+ const ch = line[i];
184
+ if (ch === '/' && line[i + 1] === '/') break;
185
+ if (ch === '/' && line[i + 1] === '*') {
186
+ const close = line.indexOf('*/', i + 2);
187
+ if (close === -1) { i = line.length; break; }
188
+ i = close + 2;
189
+ continue;
190
+ }
191
+ if (ch === "'" || ch === '"' || ch === '`') {
192
+ const quote = ch;
193
+ const start = i;
194
+ let j = i + 1;
195
+ while (j < line.length) {
196
+ if (line[j] === '\\') { j += 2; continue; }
197
+ if (line[j] === quote) { j++; break; }
198
+ j++;
199
+ }
200
+ out += line.slice(start, j);
201
+ i = j;
202
+ continue;
203
+ }
204
+ out += ch;
205
+ i++;
206
+ }
207
+ return out;
208
+ }
209
+
210
+ /**
211
+ * Scan forward from `openParenIdx` (the index of a call's opening `(`) and
212
+ * return the TEXT of its first argument — up to the first top-level comma,
213
+ * or the call's own closing paren if it has only one argument — respecting
214
+ * nested parens and quoted strings so an inner `path.join(a, 'b.md')`
215
+ * comma never terminates early. Returns null if the call does not close on
216
+ * this line (a genuinely multi-line call is out of this guard's scope — see
217
+ * module docblock).
218
+ */
219
+ function extractFirstArg(line, openParenIdx) {
220
+ let depth = 1;
221
+ let i = openParenIdx + 1;
222
+ const start = i;
223
+ while (i < line.length) {
224
+ const ch = line[i];
225
+ if (ch === "'" || ch === '"' || ch === '`') {
226
+ const quote = ch;
227
+ i++;
228
+ while (i < line.length) {
229
+ if (line[i] === '\\') { i += 2; continue; }
230
+ if (line[i] === quote) { i++; break; }
231
+ i++;
232
+ }
233
+ continue;
234
+ }
235
+ if (ch === '(') { depth++; i++; continue; }
236
+ if (ch === ')') {
237
+ if (depth === 1) return line.slice(start, i).trim();
238
+ depth--; i++; continue;
239
+ }
240
+ if (ch === ',' && depth === 1) return line.slice(start, i).trim();
241
+ i++;
242
+ }
243
+ return null; // unterminated on this line — skip (see docblock)
244
+ }
245
+
246
+ /**
247
+ * Pure: scan `text` (one `src/*.cts` file's contents) for every write call
248
+ * this guard can statically resolve to a literal `.planning/`-root file
249
+ * name. Returns EVERY resolved candidate (canonical or not) — filtering to
250
+ * violations only happens in `findArtifactWriterDrift` — so tests and
251
+ * callers can tell "not checked" (candidate absent) apart from "checked and
252
+ * passed" (candidate present, canonical).
253
+ */
254
+ function scanFileForCandidates(text, relPath) {
255
+ const file = relPath.replace(/\\/g, '/');
256
+ const originalLines = text.split('\n');
257
+ const lines = originalLines.map(stripLineComment);
258
+
259
+ // Pass 1: build the whole-file name -> file-name maps (see module
260
+ // docblock for the "flat, same-file, name-based" tracing this performs).
261
+ const rootVars = new Set();
262
+ const filenameVars = new Map();
263
+ for (const line of lines) {
264
+ const rootMatch = ROOT_VAR_ASSIGN_RE.exec(line);
265
+ if (rootMatch) rootVars.add(rootMatch[1]);
266
+ }
267
+ for (const line of lines) {
268
+ const m1 = JOIN_FROM_ROOT_CALL_RE.exec(line);
269
+ if (m1) {
270
+ const name = m1[1] || m1[2];
271
+ filenameVars.set(name, m1[3] || m1[4]);
272
+ continue;
273
+ }
274
+ const propMatch = PLANNING_PATHS_PROP_RE.exec(line);
275
+ if (propMatch) {
276
+ const name = propMatch[1] || propMatch[2];
277
+ const prop = propMatch[3];
278
+ if (PLANNING_PATHS_FILE_PROPS.has(prop)) filenameVars.set(name, PLANNING_PATHS_FILE_PROPS.get(prop));
279
+ continue;
280
+ }
281
+ const m2 = JOIN_FROM_IDENT_RE.exec(line);
282
+ if (m2) {
283
+ const name = m2[1] || m2[2];
284
+ const sourceIdent = m2[3];
285
+ if (rootVars.has(sourceIdent)) filenameVars.set(name, m2[4] || m2[5]);
286
+ }
287
+ }
288
+
289
+ // Pass 2: resolve every write call's first argument.
290
+ const out = [];
291
+ for (let li = 0; li < lines.length; li++) {
292
+ const line = lines[li];
293
+ WRITE_CALL_RE.lastIndex = 0;
294
+ let callMatch;
295
+ while ((callMatch = WRITE_CALL_RE.exec(line)) !== null) {
296
+ const openParenIdx = callMatch.index + callMatch[0].length - 1;
297
+ const argText = extractFirstArg(line, openParenIdx);
298
+ if (argText === null) continue;
299
+
300
+ let filename = null;
301
+ if (BARE_IDENT_RE.test(argText)) {
302
+ if (filenameVars.has(argText)) filename = filenameVars.get(argText);
303
+ } else {
304
+ const inlineJoin = INLINE_JOIN_ROOT_RE.exec(argText);
305
+ if (inlineJoin) {
306
+ filename = inlineJoin[1] || inlineJoin[2];
307
+ } else {
308
+ const inlineProp = INLINE_PLANNING_PATHS_PROP_RE.exec(argText);
309
+ if (inlineProp && PLANNING_PATHS_FILE_PROPS.has(inlineProp[1])) filename = PLANNING_PATHS_FILE_PROPS.get(inlineProp[1]);
310
+ }
311
+ }
312
+
313
+ if (filename !== null) {
314
+ out.push({ file, line: li + 1, filename, text: originalLines[li].trim() });
315
+ }
316
+ }
317
+ }
318
+ return out;
319
+ }
320
+
321
+ /**
322
+ * Pure: `scanFileForCandidates` filtered to violations — a resolved
323
+ * candidate whose file name `isCanonicalPlanningFile` rejects.
324
+ * `isCanonical` defaults to the REAL, compiled function (loaded lazily so a
325
+ * missing `npm run build:lib` only errors when this guard actually runs,
326
+ * not merely on `require`) but is overridable for tests that want to
327
+ * exercise the filter without a build.
328
+ */
329
+ function findArtifactWriterDrift(text, relPath, isCanonical) {
330
+ const check = isCanonical || loadIsCanonicalPlanningFile();
331
+ return scanFileForCandidates(text, relPath).filter((c) => !check(c.filename));
332
+ }
333
+
334
+ let _isCanonicalPlanningFile = null;
335
+ function loadIsCanonicalPlanningFile() {
336
+ if (_isCanonicalPlanningFile) return _isCanonicalPlanningFile;
337
+ if (!fs.existsSync(COMPILED_MODULE_PATH)) {
338
+ throw new Error(
339
+ `lint-planning-artifact-writer-drift: compiled artifact not found at ${COMPILED_MODULE_REL}.\n` +
340
+ 'Run `npm run build:lib` first.',
341
+ );
342
+ }
343
+ const mod = require(COMPILED_MODULE_PATH);
344
+ if (typeof mod.isCanonicalPlanningFile !== 'function') {
345
+ throw new Error(`lint-planning-artifact-writer-drift: ${COMPILED_MODULE_REL} does not export isCanonicalPlanningFile()`);
346
+ }
347
+ _isCanonicalPlanningFile = mod.isCanonicalPlanningFile;
348
+ return _isCanonicalPlanningFile;
349
+ }
350
+
351
+ /** Scan the authored source tree and return every writer-registry violation. */
352
+ function scanRepo(root) {
353
+ const isCanonical = loadIsCanonicalPlanningFile();
354
+ return scanTree({
355
+ root,
356
+ scanDirs: SCAN_DIRS,
357
+ scanExt: SCAN_EXT,
358
+ onFile(rel, text) {
359
+ return findArtifactWriterDrift(text, rel, isCanonical);
360
+ },
361
+ });
362
+ }
363
+
364
+ function main() {
365
+ const violations = scanRepo(REPO_ROOT);
366
+
367
+ if (violations.length === 0) {
368
+ process.stdout.write('ok planning-artifact-writer: every statically-resolvable .planning/-root write is a registered canonical artifact\n');
369
+ return;
370
+ }
371
+
372
+ process.stderr.write('planning-artifact-writer: unregistered .planning/-root artifact write(s) found.\n');
373
+ process.stderr.write('Every write of a literal .planning/-root file name must be reflected in the registry\n');
374
+ process.stderr.write("(src/artifacts.cts's isCanonicalPlanningFile, consumed by validate.health's W019):\n");
375
+ for (const v of violations) {
376
+ process.stderr.write(
377
+ ` ${sanitizeForReport(v.file)}:${v.line} '${sanitizeForReport(v.filename)}' ${sanitizeForReport(v.text)}\n` +
378
+ ` remedy: add '${sanitizeForReport(v.filename)}' to CANONICAL_EXACT in src/artifacts.cts, ` +
379
+ 'or a CANONICAL_PATTERNS regex if it is version-stamped\n',
380
+ );
381
+ }
382
+ process.exitCode = 1;
383
+ }
384
+
385
+ if (require.main === module) main();
386
+
387
+ module.exports = {
388
+ scanFileForCandidates,
389
+ findArtifactWriterDrift,
390
+ scanRepo,
391
+ stripLineComment,
392
+ extractFirstArg,
393
+ PLANNING_PATHS_FILE_PROPS,
394
+ SCAN_DIRS,
395
+ SCAN_EXT,
396
+ COMPILED_MODULE_PATH,
397
+ COMPILED_MODULE_REL,
398
+ };