@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,318 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Anti-divergence drift guard for the live-plan-counting seam
6
+ * (epic #3180, issue #3183, ADR-3180 "Planning Semantic Model Single Owner").
7
+ *
8
+ * `src/plan-scan.cts`'s `scanPhasePlans` is the SINGLE canonical owner of
9
+ * live-plan/summary counting: which files on disk are a "plan", which are a
10
+ * "summary", and how the two pair up. Every other module that reads a phase
11
+ * directory and re-derives that filename grammar itself — `readdirSync(...)`
12
+ * filtered by an inline `-PLAN.md` / `PLAN.md` / `-SUMMARY.md` / `SUMMARY.md`
13
+ * pattern — is a re-derivation that can silently drift from the owner (the
14
+ * exact failure class this epic removes; see #2349, #1988).
15
+ *
16
+ * Per ADR-3180 Decision 4(a) this guard discovers call sites by SCANNING THE
17
+ * WHOLE `src/` TREE, not by consulting an allowlist of known files — an
18
+ * allowlist only measures re-derivations in files someone remembered to
19
+ * list, and a new call site added anywhere else would sail through silently.
20
+ *
21
+ * Detection is intentionally NARROW and mirrors the existing
22
+ * `lint-phase-id-drift.cjs` precedent: a small, readable per-line regex pair
23
+ * over authored TypeScript source, with a short, explicitly-named exemption
24
+ * list — not a general-purpose AST/control-flow analysis. A line counts as a
25
+ * re-derivation when it contains BOTH:
26
+ * (a) a filename-TEST operation — `.filter(`, `.test(`, `.match(`,
27
+ * `.exec(`, `.endsWith(`, `.startsWith(`, `.includes(`, `.some(`,
28
+ * `.every(`, or `===` — and
29
+ * (b) a plan/summary filename-suffix pattern, either a quoted literal
30
+ * ('-PLAN.md', 'PLAN.md', '-SUMMARY.md', 'SUMMARY.md') OR an unquoted
31
+ * regex literal that mentions PLAN or SUMMARY and `\.md` together
32
+ * (`/-PLAN\.md$/`, `/^PLAN-\d+.*\.md$/i`)
33
+ * on the same source line. #3183 originally required (a) to be specifically
34
+ * `.filter(` on the SAME line as the literal — that missed a regex-literal
35
+ * predicate (`files.filter(f => /-PLAN\.md$/.test(f))`, no quotes) and a
36
+ * predicate defined on one line and consumed by `.filter(` on another
37
+ * (`const isPlan = f => f.endsWith('-PLAN.md'); … files.filter(isPlan)`).
38
+ * Widening (a) to any filename-test operator — not just `.filter(` itself —
39
+ * catches both: the predicate's OWN line already carries a qualifying test
40
+ * operation (`.test(`/`.endsWith(`) alongside the literal, independent of
41
+ * where `.filter(` ends up.
42
+ *
43
+ * KNOWN, ACCEPTED limits of a per-line textual scan (same tradeoff the
44
+ * phase-id-drift guard documents): a re-derivation that filters via a
45
+ * hand-rolled loop with none of the listed test operators (e.g. a manual
46
+ * character-index scan), or one whose literal and test operator are split
47
+ * across two DIFFERENT lines with no single line carrying both, is not
48
+ * caught by this narrow shape. That is left to code review, not this regex.
49
+ *
50
+ * The tree-walk / root-confinement / regex-literal-tokenizer / sanitizer
51
+ * machinery below is SHARED with `scripts/lint-milestone-window-drift.cjs`
52
+ * (#3184) via `scripts/lib/drift-scan.cjs` — see that module for the
53
+ * `isInsideRoot` case-sensitivity note, the `walk` symlink-confinement
54
+ * rationale, and the `readRegexLiteralAt` tokenizer's ReDoS-avoidance
55
+ * rationale. It is deliberately NOT duplicated here a second time (ADR-3180
56
+ * Decision 4's own "Rejected" list: "let the new drift guard copy Phase 1's
57
+ * tree-walk / root-confinement / sanitizer").
58
+ *
59
+ * A regex literal longer than MAX_REGEX_LITERAL_LEN (400) characters is not
60
+ * read, and is therefore not caught. That bound is what keeps the scan
61
+ * linear; no real plan/summary filename filter approaches it. The scan is
62
+ * scoped to SCAN_DIRS (`src`) with SCAN_EXT (.cts/.ts/.mts) — 186 files and
63
+ * 4,299 lines matching FILENAME_TEST_RE within SCAN_DIRS/SCAN_EXT as of this
64
+ * commit, 43 of them holding 7 or more backslashes and one (`src/milestone.cts`)
65
+ * holding 16. (Definition used, so this is reproducible: walk SCAN_DIRS
66
+ * filtering by SCAN_EXT exactly as `walk` does, split each file on `\n`, and
67
+ * count every line for which the exported `FILENAME_TEST_RE.test(line)` is
68
+ * true — independent of whether a PLAN/SUMMARY literal is also present on
69
+ * that line.) Those are the lines the old backtracking detector had to
70
+ * survive, and the reason the detector is now a tokenizer.
71
+ */
72
+
73
+ const path = require('node:path');
74
+ const driftScan = require('./lib/drift-scan.cjs');
75
+ const { readRegexLiteralAt, MAX_REGEX_LITERAL_LEN, isInsideRoot, sanitizeForReport, scanTree } = driftScan;
76
+
77
+ // A `.filter(` call on the line — the shape every current re-derivation uses
78
+ // to turn a directory listing into a plan-or-summary subset. Kept as its own
79
+ // export for back-compat / documentation; FILENAME_TEST_RE below is the
80
+ // broadened detector actually used (any filename-test operator, not just
81
+ // `.filter(`).
82
+ const FILTER_CALL_RE = /\.filter\(/;
83
+
84
+ // A filename-TEST operation: `.filter(`, `.test(`, `.match(`, `.exec(`,
85
+ // `.endsWith(`, `.startsWith(`, `.includes(`, `.some(`, `.every(`, or a
86
+ // strict-equality comparison. Any one of these on a line asking "is this
87
+ // filename a plan/summary" is a re-derivation, independent of whether the
88
+ // literal shows up as a `.filter(...)` predicate specifically.
89
+ const FILENAME_TEST_RE = /\.(?:filter|test|match|exec|endsWith|startsWith|includes|some|every)\(|===/;
90
+
91
+ // A quoted plan/summary filename-suffix literal: 'PLAN.md', '-PLAN.md',
92
+ // 'SUMMARY.md', or '-SUMMARY.md', single- or double-quoted (opening and
93
+ // closing quote must match).
94
+ const PLAN_SUMMARY_LITERAL_RE = /(['"])-?(?:PLAN|SUMMARY)\.md\1/;
95
+
96
+ // The two tokens that, appearing together INSIDE one regex literal, make it a
97
+ // plan/summary filename filter. `\.md` is matched as literal source text, not
98
+ // as a pattern, so there is nothing here to backtrack.
99
+ const PLAN_SUMMARY_TOKEN_RE = /PLAN|SUMMARY/i;
100
+ const ESCAPED_MD_TOKEN = '\\.md';
101
+
102
+ // Authored TypeScript source only (the generated bin/lib/*.cjs mirror it).
103
+ const SCAN_DIRS = ['src'];
104
+ const SCAN_EXT = new Set(['.cts', '.ts', '.mts']);
105
+
106
+ // The canonical owner defines the grammar; it is exempt by construction.
107
+ const OWNER_FILE = path.join('src', 'plan-scan.cts');
108
+
109
+ // core-utils.cts's canonical pairing rule (#1988/#2648): these three
110
+ // functions build/match `*-SUMMARY.md` CANDIDATE strings for a given plan —
111
+ // that IS the single pairing rule, not a re-derivation of it. Scoped to just
112
+ // these functions (not the whole file) so an unrelated re-derivation added
113
+ // elsewhere in core-utils.cts is still caught.
114
+ const CORE_UTILS_FILE = path.join('src', 'core-utils.cts');
115
+ const CORE_UTILS_EXEMPT_FUNCTIONS = new Set([
116
+ 'summaryCandidates',
117
+ 'countMatchedSummaries',
118
+ 'findUnsummarizedPlans',
119
+ 'findOrphanSummaries',
120
+ ]);
121
+
122
+ // Per ADR-3180 Decision 4(a): NOT a bare file allowlist — each entry below is
123
+ // scoped to the SPECIFIC function asking a documented, different question
124
+ // (see the inline comment at each site), so an unrelated re-derivation added
125
+ // anywhere else in these same files is still caught. Mirrors the
126
+ // CORE_UTILS_EXEMPT_FUNCTIONS mechanism above, generalized per-file.
127
+ //
128
+ // - audit.cts resolveQuickTaskSummaryFile: scans a quick task's OWN
129
+ // directory (`.planning/quick/<task>/`) for that ONE task's completion
130
+ // record — not a phase directory's live-plan/summary counting question.
131
+ // #3458 follow-up extracted this out of `scanQuickTasks` (the prior
132
+ // exemption target) into its own function so `scanQuickTasks` (read) and
133
+ // `cmdAuditAcknowledge`'s quick_tasks writer share the ONE discovery
134
+ // rule instead of each re-deriving it independently.
135
+ // - gsd2-import.cts readTasksDir: reads a FOREIGN GSD-2 legacy project's
136
+ // `tasks/` dir convention during a one-time import, not this project's
137
+ // `.planning/phases/` layout at all.
138
+ // - estimate-cli.cts collectCalibrationSamples: pairs a PLAN.md and a
139
+ // SUMMARY.md by their identical `<stem>` to build an estimation
140
+ // CALIBRATION sample (projected vs. actual token counts) — a stem-keyed
141
+ // join for a statistics question, not a live-plan/completion count.
142
+ // It intentionally does NOT use the canonical three-candidate pairing
143
+ // rule (marker-swap / `-SUMMARY.md` / extended) or exclude superseded
144
+ // plans — an unmatched or superseded plan simply yields no sample,
145
+ // which is correct for calibration, not a live-completion determination.
146
+ // - roadmap.cts cmdRoadmapAnnotateDependencies: matches a plan-ID token
147
+ // out of an ALREADY-RENDERED ROADMAP.md checklist LINE OF TEXT
148
+ // (`- [ ] 01-01-PLAN.md — …`), not a filesystem directory listing — it
149
+ // can never diverge from scanPhasePlans's file-existence rule because it
150
+ // never tests file existence at all.
151
+ // - worktree-safety.cts defaultFindSummaryFiles: a recursive walk of the
152
+ // ENTIRE `.planning/` tree (not a single phase directory) for a
153
+ // pre-merge rescue of any `*SUMMARY.md` artifact, deliberately mirroring
154
+ // the shell fallback's own `find … -name "*SUMMARY.md"` glob (quick.md,
155
+ // #2296/#2070/#2838) byte-for-behaviour rather than the phase-scoped
156
+ // plan-scan owner's root+nested rule — "rescue every summary before a
157
+ // merge blows it away" is not a live-plan/completion count.
158
+ // - planning-snapshot.cts buildPerPhasePlanScanFields (Phase 12, #3310,
159
+ // ADR-3180 §8.4): a strict `-(\d{2})-PLAN\.md$` match extracts a
160
+ // zero-padded SEQUENCE NUMBER from filenames the owner (`allPlanFiles`)
161
+ // already classified as plans, into the `perPhasePlanNumbering`
162
+ // `PlanningSnapshot` field so `validate.consistency`'s C002 rule can read
163
+ // it via the shared snapshot instead of re-scanning disk. It does not
164
+ // re-derive "is this a plan" — it answers a different, narrower question
165
+ // (does the canonical 2-digit numbering sequence have a gap) that the
166
+ // owner's boolean plan/summary classification cannot answer.
167
+ const FUNCTION_SCOPED_EXEMPTIONS = new Map([
168
+ [CORE_UTILS_FILE, CORE_UTILS_EXEMPT_FUNCTIONS],
169
+ [path.join('src', 'audit.cts'), new Set(['resolveQuickTaskSummaryFile'])],
170
+ [path.join('src', 'gsd2-import.cts'), new Set(['readTasksDir'])],
171
+ [path.join('src', 'estimate-cli.cts'), new Set(['collectCalibrationSamples'])],
172
+ [path.join('src', 'roadmap.cts'), new Set(['cmdRoadmapAnnotateDependencies'])],
173
+ [path.join('src', 'worktree-safety.cts'), new Set(['defaultFindSummaryFiles'])],
174
+ [path.join('src', 'planning-snapshot.cts'), new Set(['buildPerPhasePlanScanFields'])],
175
+ ]);
176
+
177
+ // Optional `export ` modifier: `collectCalibrationSamples` (estimate-cli.cts)
178
+ // is declared `export function …` rather than a bare `function …`, and the
179
+ // function-boundary tracker below must still recognize it for its
180
+ // FUNCTION_SCOPED_EXEMPTIONS entry above to take effect.
181
+ const TOP_LEVEL_FUNCTION_RE = /^(?:export\s+)?function\s+([A-Za-z0-9_]+)\s*\(/;
182
+
183
+ /**
184
+ * The regex literal on `line` that mentions PLAN or SUMMARY together with an
185
+ * escaped `.md` suffix — e.g. `/-PLAN\.md$/`, `/^PLAN-\d+.*\.md$/i`,
186
+ * `/-SUMMARY-\d+.*\.md$/i` — or null if there is none. Replaces the former
187
+ * `REGEX_LITERAL_MD_RE`, which was both exponentially/cubically backtracking
188
+ * (CodeQL js/redos; this guard runs in `lint:ci` on fork PRs) and unable to
189
+ * see a `[\\/]` character class.
190
+ */
191
+ function findRegexLiteralMdMatch(line) {
192
+ for (let i = 0; i < line.length; i++) {
193
+ if (line[i] !== '/') continue;
194
+ const literal = readRegexLiteralAt(line, i);
195
+ if (!literal) continue;
196
+ // Case-insensitive `\.md` test — the regex literal this replaced carried
197
+ // the `i` flag, so `\.MD`/`\.Md` must still match. A lowercased-copy
198
+ // `.includes()` preserves that behaviour without reintroducing a
199
+ // backtracking regex.
200
+ if (PLAN_SUMMARY_TOKEN_RE.test(literal.text) && literal.text.toLowerCase().includes(ESCAPED_MD_TOKEN)) {
201
+ return literal.text;
202
+ }
203
+ }
204
+ return null;
205
+ }
206
+
207
+ /**
208
+ * Strip comment text from a line before detection. A guard that fires on a
209
+ * COMMENT — including a comment documenting that the code below uses the
210
+ * canonical owner, or prose quoting this guard's own detector shapes — reports
211
+ * prose as drift and trains readers to add exemptions for documentation.
212
+ * Handles the three shapes that appear in this codebase: a whole-line
213
+ * block-comment continuation (`*` or `/*` leading), a `//` line comment, and
214
+ * a trailing `//` after code. Mirrors `lint-phase-enumeration-drift.cjs`'s
215
+ * own copy (not shared — each guard applies it at a slightly different point
216
+ * in its detection pipeline).
217
+ *
218
+ * Deliberately simple and conservative: it does not attempt full block-comment
219
+ * state tracking across lines (this is a per-line scan, same tradeoff the
220
+ * sibling guards document). A `//` inside a string literal would be stripped
221
+ * early — accepted, because the effect is to UNDER-report on a pathological
222
+ * line, never to over-report prose as drift.
223
+ */
224
+ function stripComments(line) {
225
+ const trimmed = line.trim();
226
+ // Whole-line block comment or JSDoc continuation.
227
+ if (trimmed.startsWith('*') || trimmed.startsWith('/*') || trimmed.startsWith('//')) return '';
228
+ // Trailing line comment after code.
229
+ const idx = line.indexOf('//');
230
+ return idx === -1 ? line : line.slice(0, idx);
231
+ }
232
+
233
+ /**
234
+ * Pure: find every unsanctioned plan/summary-filter re-derivation in `text`.
235
+ * `relPath` is the repo-relative path, used both to report file:line and to
236
+ * apply the narrow, function-scoped core-utils.cts exemption.
237
+ * Returns [{ line, found }].
238
+ */
239
+ function findPlanCountDrift(text, relPath) {
240
+ const out = [];
241
+ const lines = text.split('\n');
242
+ const exemptFunctions = FUNCTION_SCOPED_EXEMPTIONS.get(relPath) || null;
243
+ let currentFunction = null;
244
+ for (let i = 0; i < lines.length; i++) {
245
+ const line = lines[i];
246
+ const fnMatch = TOP_LEVEL_FUNCTION_RE.exec(line);
247
+ if (fnMatch) currentFunction = fnMatch[1];
248
+
249
+ const code = stripComments(line);
250
+ if (!code.trim()) continue;
251
+
252
+ if (!FILENAME_TEST_RE.test(code)) continue;
253
+ const quoted = PLAN_SUMMARY_LITERAL_RE.exec(code);
254
+ const found = quoted ? quoted[0] : findRegexLiteralMdMatch(code);
255
+ if (!found) continue;
256
+
257
+ if (exemptFunctions && exemptFunctions.has(currentFunction)) continue;
258
+
259
+ out.push({ line: i + 1, found });
260
+ }
261
+ return out;
262
+ }
263
+
264
+ /**
265
+ * Scan the authored source tree and return every unsanctioned re-derivation,
266
+ * each annotated with the repo-relative file path.
267
+ */
268
+ function scanRepo(root) {
269
+ return scanTree({
270
+ root,
271
+ scanDirs: SCAN_DIRS,
272
+ scanExt: SCAN_EXT,
273
+ onFile(rel, text) {
274
+ // `rel` is already the REAL (canonical) path (scanTree resolves
275
+ // symlinks before calling onFile), so this comparison — and
276
+ // FUNCTION_SCOPED_EXEMPTIONS above, also keyed on `rel` — match
277
+ // consistently regardless of which symlink reached the file.
278
+ if (rel === OWNER_FILE) return [];
279
+ return findPlanCountDrift(text, rel).map((d) => ({ file: rel, ...d }));
280
+ },
281
+ });
282
+ }
283
+
284
+ function main() {
285
+ const root = path.join(__dirname, '..');
286
+ const violations = scanRepo(root);
287
+ if (violations.length === 0) {
288
+ process.stdout.write('ok plan-count-drift: no unsanctioned plan/summary re-derivations outside plan-scan.cts\n');
289
+ return;
290
+ }
291
+ process.stderr.write('plan-count-drift: independent re-derivation(s) of plan/summary filename filtering found.\n');
292
+ process.stderr.write('Use src/plan-scan.cjs `scanPhasePlans` (or core-utils.cjs `getPhaseFileStats`, which now\n');
293
+ process.stderr.write('sources plans/summaries from it) instead of re-deriving the -PLAN.md/-SUMMARY.md filter:\n');
294
+ for (const d of violations) {
295
+ // `d.file` is exactly as attacker-controlled as `d.found`: a repo can
296
+ // legally track a filename containing control bytes / bidi overrides,
297
+ // and it is a fork-PR-authored value reaching a CI log the same way the
298
+ // matched literal does — sanitize it at the same reporting boundary.
299
+ process.stderr.write(` ${sanitizeForReport(d.file)}:${d.line} ${sanitizeForReport(d.found)}\n`);
300
+ }
301
+ process.exitCode = 1;
302
+ }
303
+
304
+ if (require.main === module) main();
305
+
306
+ module.exports = {
307
+ findPlanCountDrift,
308
+ scanRepo,
309
+ FILTER_CALL_RE,
310
+ FILENAME_TEST_RE,
311
+ PLAN_SUMMARY_LITERAL_RE,
312
+ findRegexLiteralMdMatch,
313
+ readRegexLiteralAt,
314
+ MAX_REGEX_LITERAL_LEN,
315
+ isInsideRoot,
316
+ sanitizeForReport,
317
+ stripComments,
318
+ };