@opengsd/gsd-core 1.18.0 → 1.20.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 (308) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -1
  3. package/.opencode/plugins/gsd-core.js +2 -3
  4. package/agents/gsd-code-fixer.compact.md +7 -7
  5. package/agents/gsd-code-fixer.md +13 -7
  6. package/agents/gsd-code-reviewer.compact.md +49 -8
  7. package/agents/gsd-code-reviewer.md +53 -14
  8. package/agents/gsd-executor.md +8 -4
  9. package/agents/gsd-verifier.md +1 -1
  10. package/bin/install.js +422 -103
  11. package/commands/gsd/update.md +5 -2
  12. package/gsd-core/bin/gsd-tools.cjs +179 -30
  13. package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
  14. package/gsd-core/bin/lib/args-terminator.cjs +114 -0
  15. package/gsd-core/bin/lib/broken-windows.cjs +59 -1
  16. package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
  17. package/gsd-core/bin/lib/capability-lock.cjs +13 -2
  18. package/gsd-core/bin/lib/capability-registry.cjs +376 -103
  19. package/gsd-core/bin/lib/capability-state.cjs +14 -8
  20. package/gsd-core/bin/lib/capability-validator.cjs +32 -0
  21. package/gsd-core/bin/lib/commands.cjs +27 -17
  22. package/gsd-core/bin/lib/config-loader.cjs +13 -3
  23. package/gsd-core/bin/lib/config.cjs +256 -58
  24. package/gsd-core/bin/lib/core-utils.cjs +45 -0
  25. package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
  26. package/gsd-core/bin/lib/frontmatter.cjs +21 -3
  27. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
  28. package/gsd-core/bin/lib/gate-context-drift.cjs +9 -4
  29. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +6 -1
  30. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +6 -1
  31. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
  32. package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
  33. package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
  34. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +63 -22
  35. package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
  36. package/gsd-core/bin/lib/git-info-exclude.cjs +52 -0
  37. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +31 -12
  38. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
  39. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
  40. package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
  41. package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
  42. package/gsd-core/bin/lib/host-integration.cjs +13 -6
  43. package/gsd-core/bin/lib/init.cjs +245 -68
  44. package/gsd-core/bin/lib/install-engine.cjs +246 -32
  45. package/gsd-core/bin/lib/install-profiles.cjs +9 -4
  46. package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
  47. package/gsd-core/bin/lib/io.cjs +6 -0
  48. package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
  49. package/gsd-core/bin/lib/markdown-sectionizer.cjs +147 -1
  50. package/gsd-core/bin/lib/markdown-table.cjs +23 -0
  51. package/gsd-core/bin/lib/mcp-server.cjs +8 -5
  52. package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
  53. package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
  54. package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
  55. package/gsd-core/bin/lib/milestone.cjs +61 -51
  56. package/gsd-core/bin/lib/model-catalog.cjs +8 -0
  57. package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
  58. package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
  59. package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
  60. package/gsd-core/bin/lib/phase-id.cjs +629 -38
  61. package/gsd-core/bin/lib/phase-lifecycle.cjs +12 -44
  62. package/gsd-core/bin/lib/phase-locator.cjs +328 -19
  63. package/gsd-core/bin/lib/phase.cjs +3066 -653
  64. package/gsd-core/bin/lib/planning-document.cjs +26 -5
  65. package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
  66. package/gsd-core/bin/lib/planning-snapshot.cjs +15 -8
  67. package/gsd-core/bin/lib/planning-workspace.cjs +21 -3
  68. package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
  69. package/gsd-core/bin/lib/read-only-git-env.cjs +79 -0
  70. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
  71. package/gsd-core/bin/lib/review-lane-descriptor.cjs +57 -2
  72. package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
  73. package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
  74. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
  75. package/gsd-core/bin/lib/roadmap-parser.cjs +735 -137
  76. package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
  77. package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -7
  78. package/gsd-core/bin/lib/roadmap.cjs +383 -123
  79. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +381 -14
  80. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
  81. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
  82. package/gsd-core/bin/lib/shell-command-projection.cjs +151 -19
  83. package/gsd-core/bin/lib/smart-entry.cjs +35 -11
  84. package/gsd-core/bin/lib/state-contract.cjs +39 -15
  85. package/gsd-core/bin/lib/state-document.cjs +344 -27
  86. package/gsd-core/bin/lib/state-transition.cjs +156 -32
  87. package/gsd-core/bin/lib/state.cjs +527 -186
  88. package/gsd-core/bin/lib/surface.cjs +9 -0
  89. package/gsd-core/bin/lib/task-command-router.cjs +5 -25
  90. package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
  91. package/gsd-core/bin/lib/template.cjs +2 -2
  92. package/gsd-core/bin/lib/validate.cjs +3 -2
  93. package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
  94. package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
  95. package/gsd-core/bin/lib/verification.cjs +96 -9
  96. package/gsd-core/bin/lib/verify.cjs +4 -2
  97. package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
  98. package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
  99. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  100. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  101. package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
  102. package/gsd-core/references/cicd-review-patterns.md +45 -0
  103. package/gsd-core/references/git-integration.md +11 -0
  104. package/gsd-core/references/loop-hook-dispatch.md +13 -6
  105. package/gsd-core/references/necessity-review.md +48 -0
  106. package/gsd-core/references/planning-config.md +2 -0
  107. package/gsd-core/references/tdd.md +2 -2
  108. package/gsd-core/templates/DEBUG.md +16 -0
  109. package/gsd-core/templates/config.json +2 -1
  110. package/gsd-core/templates/verification-report.md +1 -1
  111. package/gsd-core/workflows/audit-fix.md +2 -1
  112. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
  113. package/gsd-core/workflows/code-review-fix.md +46 -5
  114. package/gsd-core/workflows/code-review.md +64 -7
  115. package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
  116. package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
  117. package/gsd-core/workflows/complete-milestone.md +37 -150
  118. package/gsd-core/workflows/diagnose-issues.md +29 -5
  119. package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
  120. package/gsd-core/workflows/discuss-phase-assumptions.md +33 -2
  121. package/gsd-core/workflows/discuss-phase-power.md +19 -2
  122. package/gsd-core/workflows/discuss-phase.md +6 -10
  123. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
  124. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +7 -2
  125. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -1
  126. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +2 -2
  127. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
  128. package/gsd-core/workflows/execute-phase.md +10 -6
  129. package/gsd-core/workflows/execute-plan.md +1 -1
  130. package/gsd-core/workflows/health.md +2 -1
  131. package/gsd-core/workflows/import.md +1 -1
  132. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +2 -2
  133. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
  134. package/gsd-core/workflows/plan-phase.md +14 -14
  135. package/gsd-core/workflows/pr-branch.md +2 -0
  136. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
  137. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
  138. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
  139. package/gsd-core/workflows/quick.md +1 -1
  140. package/gsd-core/workflows/reapply-patches.md +104 -0
  141. package/gsd-core/workflows/review.md +70 -154
  142. package/gsd-core/workflows/section-manifest.json +5 -0
  143. package/gsd-core/workflows/secure-phase.md +1 -1
  144. package/gsd-core/workflows/settings-advanced.md +57 -9
  145. package/gsd-core/workflows/settings.md +1 -0
  146. package/gsd-core/workflows/ship.md +15 -4
  147. package/gsd-core/workflows/spec-phase.md +3 -6
  148. package/gsd-core/workflows/verify-work.md +4 -3
  149. package/hooks/dist/gsd-agent-isolation-guard.js +11 -1
  150. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  151. package/hooks/dist/gsd-graphify-update.sh +6 -0
  152. package/hooks/dist/gsd-path-export.js +95 -0
  153. package/hooks/dist/gsd-secret-read-guard.js +172 -39
  154. package/hooks/dist/gsd-statusline.js +37 -3
  155. package/hooks/dist/gsd-windsurf-pre-write.js +29 -2
  156. package/hooks/dist/gsd-workflow-guard.js +3 -2
  157. package/hooks/dist/gsd-worktree-path-guard.js +29 -2
  158. package/hooks/dist/lib/git-probe.js +16 -0
  159. package/hooks/dist/lib/isolation-sentinel.js +44 -0
  160. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  161. package/hooks/gsd-agent-isolation-guard.js +11 -1
  162. package/hooks/gsd-cursor-subagent-start.js +6 -2
  163. package/hooks/gsd-graphify-update.sh +6 -0
  164. package/hooks/gsd-path-export.js +95 -0
  165. package/hooks/gsd-secret-read-guard.js +172 -39
  166. package/hooks/gsd-statusline.js +37 -3
  167. package/hooks/gsd-windsurf-pre-write.js +29 -2
  168. package/hooks/gsd-workflow-guard.js +3 -2
  169. package/hooks/gsd-worktree-path-guard.js +29 -2
  170. package/hooks/hooks.json +1 -0
  171. package/hooks/lib/git-probe.js +16 -0
  172. package/hooks/lib/isolation-sentinel.js +44 -0
  173. package/hooks/managed-hooks-registry.cjs +1 -0
  174. package/package.json +15 -8
  175. package/pi/gsd.cjs +1047 -10
  176. package/pi/package-agents/gsd-advisor-researcher.md +113 -0
  177. package/pi/package-agents/gsd-ai-researcher.md +111 -0
  178. package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
  179. package/pi/package-agents/gsd-code-fixer.md +774 -0
  180. package/pi/package-agents/gsd-code-reviewer.md +441 -0
  181. package/pi/package-agents/gsd-codebase-mapper.md +850 -0
  182. package/pi/package-agents/gsd-debug-session-manager.md +416 -0
  183. package/pi/package-agents/gsd-debugger.md +1274 -0
  184. package/pi/package-agents/gsd-doc-classifier.md +269 -0
  185. package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
  186. package/pi/package-agents/gsd-doc-verifier.md +212 -0
  187. package/pi/package-agents/gsd-doc-writer.md +626 -0
  188. package/pi/package-agents/gsd-dom-verifier.md +164 -0
  189. package/pi/package-agents/gsd-domain-researcher.md +144 -0
  190. package/pi/package-agents/gsd-eval-auditor.md +185 -0
  191. package/pi/package-agents/gsd-eval-planner.md +149 -0
  192. package/pi/package-agents/gsd-executor.md +898 -0
  193. package/pi/package-agents/gsd-framework-selector.md +159 -0
  194. package/pi/package-agents/gsd-integration-checker.md +476 -0
  195. package/pi/package-agents/gsd-intel-updater.md +338 -0
  196. package/pi/package-agents/gsd-mempalace-curator.md +49 -0
  197. package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
  198. package/pi/package-agents/gsd-pattern-mapper.md +340 -0
  199. package/pi/package-agents/gsd-phase-researcher.md +897 -0
  200. package/pi/package-agents/gsd-plan-checker.md +1077 -0
  201. package/pi/package-agents/gsd-planner.md +1003 -0
  202. package/pi/package-agents/gsd-project-researcher.md +611 -0
  203. package/pi/package-agents/gsd-research-synthesizer.md +259 -0
  204. package/pi/package-agents/gsd-roadmapper.md +779 -0
  205. package/pi/package-agents/gsd-security-auditor.md +171 -0
  206. package/pi/package-agents/gsd-ui-auditor.md +581 -0
  207. package/pi/package-agents/gsd-ui-checker.md +417 -0
  208. package/pi/package-agents/gsd-ui-researcher.md +469 -0
  209. package/pi/package-agents/gsd-user-profiler.md +174 -0
  210. package/pi/package-agents/gsd-verifier.md +963 -0
  211. package/pi/package-agents/manifest.json +39 -0
  212. package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
  213. package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
  214. package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
  215. package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
  216. package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
  217. package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
  218. package/pi/package-skills/gsd-capture/SKILL.md +68 -0
  219. package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
  220. package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
  221. package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
  222. package/pi/package-skills/gsd-config/SKILL.md +60 -0
  223. package/pi/package-skills/gsd-debug/SKILL.md +64 -0
  224. package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
  225. package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
  226. package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
  227. package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
  228. package/pi/package-skills/gsd-explore/SKILL.md +26 -0
  229. package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
  230. package/pi/package-skills/gsd-fast/SKILL.md +33 -0
  231. package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
  232. package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
  233. package/pi/package-skills/gsd-health/SKILL.md +36 -0
  234. package/pi/package-skills/gsd-help/SKILL.md +37 -0
  235. package/pi/package-skills/gsd-import/SKILL.md +46 -0
  236. package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
  237. package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
  238. package/pi/package-skills/gsd-manager/SKILL.md +46 -0
  239. package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
  240. package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
  241. package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
  242. package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
  243. package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
  244. package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
  245. package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
  246. package/pi/package-skills/gsd-next/SKILL.md +34 -0
  247. package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
  248. package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
  249. package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
  250. package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
  251. package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
  252. package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
  253. package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
  254. package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
  255. package/pi/package-skills/gsd-phase/SKILL.md +61 -0
  256. package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
  257. package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
  258. package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
  259. package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
  260. package/pi/package-skills/gsd-progress/SKILL.md +51 -0
  261. package/pi/package-skills/gsd-quick/SKILL.md +179 -0
  262. package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
  263. package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
  264. package/pi/package-skills/gsd-review/SKILL.md +50 -0
  265. package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
  266. package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
  267. package/pi/package-skills/gsd-settings/SKILL.md +30 -0
  268. package/pi/package-skills/gsd-ship/SKILL.md +27 -0
  269. package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
  270. package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
  271. package/pi/package-skills/gsd-spike/SKILL.md +53 -0
  272. package/pi/package-skills/gsd-stats/SKILL.md +22 -0
  273. package/pi/package-skills/gsd-surface/SKILL.md +177 -0
  274. package/pi/package-skills/gsd-thread/SKILL.md +29 -0
  275. package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
  276. package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
  277. package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
  278. package/pi/package-skills/gsd-undo/SKILL.md +38 -0
  279. package/pi/package-skills/gsd-update/SKILL.md +54 -0
  280. package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
  281. package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
  282. package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
  283. package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
  284. package/scripts/build-hooks.js +4 -0
  285. package/scripts/ci-next-health.cjs +246 -23
  286. package/scripts/command-contract-helpers.cjs +1 -1
  287. package/scripts/docs-guard-registry.cjs +22 -7
  288. package/scripts/gen-features.cjs +14 -2
  289. package/scripts/gen-health-docs.cjs +13 -1
  290. package/scripts/gen-pi-package-skills.cjs +217 -0
  291. package/scripts/lib/drift-scan.cjs +27 -0
  292. package/scripts/lib/macos-conformance-tier.generated.cjs +17 -1
  293. package/scripts/lib/platform-conformance-tier.generated.cjs +37 -1
  294. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
  295. package/scripts/lint-phase-enumeration-drift.cjs +8 -4
  296. package/scripts/lint-retired-runtime-name.cjs +5 -0
  297. package/scripts/lint-roadmap-progress-drift.cjs +421 -0
  298. package/scripts/mutation-matrix.cjs +23 -1
  299. package/scripts/npm-audit-baseline.cjs +41 -7
  300. package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
  301. package/scripts/select-docs-guards.cjs +1 -1
  302. package/scripts/self-test-report-policy.cjs +350 -0
  303. package/scripts/sync-next-version.cjs +38 -6
  304. package/skills/gsd-update/SKILL.md +5 -2
  305. package/vscode/package.json +1 -1
  306. package/scripts/diagnose-validate-commit-in-tier.cjs +0 -270
  307. package/scripts/diagnose-validate-commit-phases.cjs +0 -348
  308. package/scripts/xtrace-phase-timing.cjs +0 -205
@@ -0,0 +1,421 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Anti-divergence drift guard for the ROADMAP Progress-table and phase-entry
6
+ * seams (epic #5692, issue #5765, ADR-2143 section 7).
7
+ *
8
+ * Two modules are the only legitimate home of two questions:
9
+ * - `src/roadmap-progress.cts` "which table is the Progress table, and what
10
+ * is its milestone slice" (Phases 1-2: readers and writers).
11
+ * - `src/phase-entry-grammar.cts` "what is a phase-list entry" (bullet marker,
12
+ * checkbox, bold token, anchoring) (Phases 3-4: readers and writers).
13
+ * The census in #5703 found 12 hand-written Progress locators and 26 hand-written
14
+ * phase-entry patterns that had drifted apart. After Phases 1-4 none remain
15
+ * outside the owners. This guard keeps it that way: the next local `## Progress`
16
+ * regex, Progress-table `.replace()` or `- [ ] Phase` pattern fails `lint:ci`.
17
+ *
18
+ * Four classes are detected, per source line, with comments stripped first (a
19
+ * comment documenting an owner is not a re-derivation of it):
20
+ * progress-heading a regex literal, or a string literal, that spells a
21
+ * level-N heading followed by `Progress` using regex
22
+ * syntax (`/^##[ \t]+Progress\b/im`, `#{1,3}\s*Progress`).
23
+ * progress-title the bare title test (`/^Progress\b/i`, `'Progress\\b'`)
24
+ * that the bracket writers used to share.
25
+ * progress-table-replace `.replace(` / `.replaceAll(` whose FIRST argument is a
26
+ * Progress-table pattern: a regex naming `Progress` or
27
+ * `Plans Complete`, a table-row regex (two or more
28
+ * escaped pipes) on a line that names ROADMAP content, or
29
+ * an identifier such as `progressRowRe` (ADR-2143
30
+ * section 7(b)). It looks at the first argument only, so
31
+ * `roadmap.title.replace(/\|/g, '\\|')` is not flagged.
32
+ * phase-entry-checkbox a literal that carries an escaped checkbox class
33
+ * (`\[[ xX]\]`, `\[( |x)\]`, `\[x\]`, `\[.\]`) AND the word
34
+ * `Phase` (`- [ ] **Phase 1: Name**`).
35
+ *
36
+ * What is NOT flagged, on purpose: a heading BUILT for output (`'## Progress\n'`,
37
+ * `'## **Progress**\n'`: no regex syntax), the STATE.md `Progress:` field, status
38
+ * vocabulary (`IN PROGRESS`, `/^(done|progress\b)/`), the heading bracket tag
39
+ * (`### [tag] Phase N:`) and requirement / plan-row checkboxes (no `Phase`). Those
40
+ * answer different questions owned elsewhere.
41
+ *
42
+ * Owner exemption is per class, never per file: `roadmap-progress.cts` defines the
43
+ * first three classes and `phase-entry-grammar.cts` defines the fourth. Each owner
44
+ * is still scanned for the classes it does not own, so a stray pattern added to an
45
+ * owner is caught (ADR-3180 Decision 4(d)). There is no allowlist file and no
46
+ * function-scoped exemption: Phases 1-4 left nothing to allow.
47
+ *
48
+ * The guard carries its own positive control. Every run first feeds a fixture
49
+ * violation of each class through the same detector and requires it to be flagged,
50
+ * and feeds clean fixtures and requires silence. A guard that cannot go red must
51
+ * not report green, so a detector that stops matching fails `lint:ci` here, in the
52
+ * same run, not only in a test file nobody reads. The tree-walk, root confinement,
53
+ * literal tokenizers and report sanitizer are shared with the sibling drift guards
54
+ * through `scripts/lib/drift-scan.cjs`.
55
+ *
56
+ * KNOWN, ACCEPTED limits of a per-line textual scan (same tradeoff the sibling
57
+ * guards document): a pattern whose tokens sit on different lines, one assembled
58
+ * from several literals, one routed through dynamic dispatch, or a `.replace()` on a
59
+ * variable whose name says neither ROADMAP nor Progress is not caught. The detector
60
+ * is name-sensitive on purpose: it prefers a missed exotic shape to a false
61
+ * positive on ordinary new code, because a guard that cries wolf gets exempted. The
62
+ * cross-site property tests of Phases 1-4 and code review cover the rest.
63
+ */
64
+
65
+ const path = require('node:path');
66
+ const driftScan = require('./lib/drift-scan.cjs');
67
+ const { readRegexLiteralAt, readStringLiteralAt, MAX_REGEX_LITERAL_LEN, sanitizeForReport, scanTree } = driftScan;
68
+
69
+ // Authored source and hooks. Generated mirrors (`gsd-core/bin/lib/*.cjs`) are
70
+ // build output of `src/`; `scripts/` carries these tokens as detector text and
71
+ // `tests/` builds ROADMAP fixtures on purpose.
72
+ const SCAN_DIRS = ['src', 'hooks'];
73
+ const SCAN_EXT = new Set(['.cts', '.ts', '.mts', '.cjs', '.js', '.mjs']);
74
+
75
+ const CLASS = Object.freeze({
76
+ PROGRESS_HEADING: 'progress-heading',
77
+ PROGRESS_TITLE: 'progress-title',
78
+ PROGRESS_TABLE_REPLACE: 'progress-table-replace',
79
+ PHASE_ENTRY_CHECKBOX: 'phase-entry-checkbox',
80
+ });
81
+
82
+ const PROGRESS_OWNER = 'src/roadmap-progress.cts';
83
+ const GRAMMAR_OWNER = 'src/phase-entry-grammar.cts';
84
+
85
+ // Which classes each owner DEFINES (and so may spell). Everything else about an
86
+ // owner file is scanned like any other file.
87
+ const OWNED_CLASSES = new Map([
88
+ [PROGRESS_OWNER, new Set([CLASS.PROGRESS_HEADING, CLASS.PROGRESS_TITLE, CLASS.PROGRESS_TABLE_REPLACE])],
89
+ [GRAMMAR_OWNER, new Set([CLASS.PHASE_ENTRY_CHECKBOX])],
90
+ ]);
91
+
92
+ // A level-N heading marker (`#`, `##`, `#+`, `#*`, `#{1,3}`) followed by whitespace
93
+ // tokens, classes or a quantified gap (`\s*`, `[ \t]+`, ` {1,3}`, a space) and then
94
+ // the word `Progress`. The gap group is a bounded repeat of alternatives that
95
+ // cannot consume the same character two ways (a quantifier belongs to the token
96
+ // before it, never to the group), so matching is linear in the literal length: a
97
+ // repeat of `*` or `+` as its own alternative would make `\s*\s*\s*...` ambiguous
98
+ // and exponential (CodeQL js/redos runs on `lint:ci`).
99
+ const HEADING_THEN_PROGRESS_RE =
100
+ /#(?:\{\d{1,2},\d{1,2}\}|#{0,5}|[+*])(?:\\{1,2}[sSh][*+?]?|\[[ \\t]{1,6}\][*+?]?|\{\d{1,2}(?:,\d{0,2})?\}| ){0,12}[*_]{0,2}(?:\(\?:|\()?Progress/i;
101
+ // The tail the bold marker and the group opener add (`**Progress`, `(?:Progress`):
102
+ // not regex syntax in the gap, so it is cut before the syntax test.
103
+ const PROGRESS_TAIL_RE = /[*_]{0,2}(?:\(\?:|\()?Progress$/i;
104
+ // Regex syntax inside the heading gap: an escape, a class, a quantifier brace, a
105
+ // repeat. A plain `## Progress` (or `## **Progress**`) is a heading someone is WRITING.
106
+ const REGEX_SYNTAX_RE = /[\\[\]{}+*]/;
107
+ const CARET_BEFORE_HASH_RE = /\^[ \t]*#/;
108
+
109
+ // The bare Progress title test: `Progress` followed by a word boundary, right after
110
+ // a quote, a slash or an anchor. Not after `|`: `/^(done|progress\b)/` is status
111
+ // vocabulary, not a heading test.
112
+ const PROGRESS_TITLE_RE = /(?:^['"`/]|\^)(?:\\{1,2}s[*+]?)?Progress\\{1,2}b/i;
113
+
114
+ const REPLACE_CALL_G = /\.replace(?:All)?\s*\(/g;
115
+ const ROADMAP_WORD_RE = /roadmap/i;
116
+ // A first-argument identifier that names a Progress table pattern: `progressRowRe`,
117
+ // `PROGRESS_TABLE_RE`, `progressSectionPattern`.
118
+ const PROGRESS_PATTERN_IDENT_RE = /progress[_\w$]{0,40}?(?:table|row|section|heading)/i;
119
+ const PROGRESS_WORD_RE = /progress/i;
120
+ // The Progress table's own header text (`Plans Complete`), with a literal or escaped space.
121
+ const PLANS_COMPLETE_RE = /Plans(?: |\\{1,2}s[*+]?)Complete/i;
122
+ const ESCAPED_PIPE_G = /\\{1,2}\|/g;
123
+
124
+ // A checkbox written as regex syntax: `\[` + a box interior + `\]`. The interior is
125
+ // 1 to 12 characters drawn from the box alphabet: a space, `x`/`X`, `.`, `\s`, class
126
+ // and group punctuation and the quantifiers (`[ xX]`, `[x ]`, `( |x)`, `([xX ])`,
127
+ // ` ?[xX]? ?`, `.`). Letters other than x/X, digits and `^` are outside it, so a
128
+ // markdown link such as `\[Phase 1\]` and the heading bracket tag
129
+ // `\[[^\]]{1,200}\]` (a different seam: `### [tag] Phase N:`) are not checkboxes. A
130
+ // checkbox literal in a template that EMITS an entry (`- [ ] **Phase 1**`) has no
131
+ // backslashes and is not a pattern.
132
+ const ESCAPED_CHECKBOX_RE = /\\{1,2}\[[ xX.\\s?*+|()[\]-]{1,12}\\{1,2}\]/;
133
+ // The word `Phase`, case-insensitive, not inside a longer word or a hyphenated name
134
+ // (`Phase-gate`), and also right after a regex class escape (`\sPhase`).
135
+ const PHASE_WORD_RE = /(?:^|[^A-Za-z]|\\[sSwWdDbB])Phase(?![A-Za-z-])/i;
136
+
137
+ /**
138
+ * Strip comment text before detection: a guard that fires on a comment trains
139
+ * readers to add exemptions for documentation. Whole-line `//` comments and block
140
+ * comment continuations are dropped, a block comment opened and closed on the line
141
+ * is cut out, and a trailing `//` after code is cut. Quote- and regex-aware, so
142
+ * `https://` in a string or `//` inside a regex literal does not cut the line (the
143
+ * older guards use `indexOf('//')` and over-cut). Kept local rather than shared
144
+ * because each guard applies comment stripping at a different point of its
145
+ * pipeline and the variants differ on purpose.
146
+ */
147
+ function stripComments(line) {
148
+ const trimmed = line.trim();
149
+ if (trimmed.startsWith('/*')) {
150
+ const close = line.indexOf('*/');
151
+ return close === -1 ? '' : stripComments(line.slice(close + 2));
152
+ }
153
+ if (trimmed.startsWith('*') || trimmed.startsWith('//')) return '';
154
+ let quote = null;
155
+ for (let i = 0; i < line.length; i++) {
156
+ const ch = line[i];
157
+ if (quote) {
158
+ if (ch === '\\') i++;
159
+ else if (ch === quote) quote = null;
160
+ continue;
161
+ }
162
+ if (ch === "'" || ch === '"' || ch === '`') quote = ch;
163
+ else if (ch === '/' && line[i + 1] === '/') return line.slice(0, i);
164
+ else if (ch === '/' && line[i + 1] !== '*') {
165
+ // A `/` may open a regex literal or be a division. A real literal is skipped so
166
+ // a `//` inside it is not read as a comment; a division reads as a "literal"
167
+ // that closes at the next `/`, which only ever skips code, never a comment
168
+ // opener that follows it on the same line (that case is re-checked below).
169
+ const re = readRegexLiteralAt(line, i);
170
+ if (re && !re.text.slice(1, -1).includes('//')) i = re.end - 1;
171
+ }
172
+ }
173
+ return line;
174
+ }
175
+
176
+ /**
177
+ * Every regex literal and quoted/backtick string literal on `line`, in order. A `/`
178
+ * is either a regex opener or a division, and the two cannot be told apart without
179
+ * a parser, so each `/` yields a CANDIDATE literal and scanning resumes just after
180
+ * the opening `/` (candidates overlap): `done / total, re = /^##\s+Progress/m`
181
+ * yields the division pseudo-literal and the real regex. Strings are exact, so a
182
+ * string literal is consumed whole.
183
+ */
184
+ function literalsOn(line) {
185
+ const out = [];
186
+ for (let i = 0; i < line.length; i++) {
187
+ const ch = line[i];
188
+ if (ch === '/') {
189
+ const literal = readRegexLiteralAt(line, i);
190
+ if (literal) out.push(literal.text);
191
+ } else if (ch === "'" || ch === '"' || ch === '`') {
192
+ const literal = readStringLiteralAt(line, i);
193
+ if (literal) {
194
+ out.push(literal.text);
195
+ i = literal.end - 1;
196
+ }
197
+ }
198
+ }
199
+ return out;
200
+ }
201
+
202
+ function isProgressHeadingLiteral(text) {
203
+ const match = HEADING_THEN_PROGRESS_RE.exec(text);
204
+ if (!match) return false;
205
+ const gap = match[0].slice(1).replace(PROGRESS_TAIL_RE, '');
206
+ return REGEX_SYNTAX_RE.test(gap) || CARET_BEFORE_HASH_RE.test(text);
207
+ }
208
+
209
+ /**
210
+ * The first argument of the `.replace(` / `.replaceAll(` call that opens at
211
+ * `code[open]` (the index just past the `(`): a regex literal, a string literal or
212
+ * an identifier path. Null when the argument is anything else.
213
+ */
214
+ function firstReplaceArgument(code, open) {
215
+ let i = open;
216
+ while (code[i] === ' ' || code[i] === '\t') i++;
217
+ const ch = code[i];
218
+ if (ch === '/') {
219
+ const literal = readRegexLiteralAt(code, i);
220
+ return literal ? { kind: 'regex', text: literal.text } : null;
221
+ }
222
+ if (ch === "'" || ch === '"' || ch === '`') {
223
+ const literal = readStringLiteralAt(code, i);
224
+ return literal ? { kind: 'string', text: literal.text } : null;
225
+ }
226
+ const ident = /^[A-Za-z_$][\w$.]*/.exec(code.slice(i, i + 120));
227
+ return ident ? { kind: 'identifier', text: ident[0] } : null;
228
+ }
229
+
230
+ function isProgressTableReplace(code) {
231
+ const namesRoadmap = ROADMAP_WORD_RE.test(code);
232
+ for (const call of code.matchAll(REPLACE_CALL_G)) {
233
+ const arg = firstReplaceArgument(code, call.index + call[0].length);
234
+ if (!arg) continue;
235
+ if (arg.kind === 'identifier') {
236
+ if (PROGRESS_PATTERN_IDENT_RE.test(arg.text) || (namesRoadmap && PROGRESS_WORD_RE.test(arg.text))) return true;
237
+ continue;
238
+ }
239
+ if (PLANS_COMPLETE_RE.test(arg.text)) return true;
240
+ if (namesRoadmap && (PROGRESS_WORD_RE.test(arg.text) || (arg.text.match(ESCAPED_PIPE_G) || []).length >= 2)) return true;
241
+ }
242
+ return false;
243
+ }
244
+
245
+ /**
246
+ * Pure: every unsanctioned Progress / phase-entry pattern in `text`.
247
+ * `relPath` is the repo-relative POSIX path, used to report file:line and to apply
248
+ * the per-class owner exemption. Returns [{ line, cls, found }], where `found` is
249
+ * the matched literal (or the offending line for the replace class).
250
+ */
251
+ function findProgressDrift(text, relPath) {
252
+ const owned = OWNED_CLASSES.get(relPath) || new Set();
253
+ const out = [];
254
+ const lines = text.split('\n');
255
+ for (let i = 0; i < lines.length; i++) {
256
+ const code = stripComments(lines[i]);
257
+ if (!code.trim()) continue;
258
+ const literals = literalsOn(code);
259
+ const report = (cls, found) => {
260
+ if (!owned.has(cls)) out.push({ line: i + 1, cls, found: found.slice(0, MAX_REGEX_LITERAL_LEN) });
261
+ };
262
+
263
+ const headingLiteral = literals.find(isProgressHeadingLiteral);
264
+ if (headingLiteral !== undefined) report(CLASS.PROGRESS_HEADING, headingLiteral);
265
+
266
+ const titleLiteral = literals.find((t) => PROGRESS_TITLE_RE.test(t));
267
+ if (titleLiteral !== undefined) report(CLASS.PROGRESS_TITLE, titleLiteral);
268
+
269
+ if (isProgressTableReplace(code)) report(CLASS.PROGRESS_TABLE_REPLACE, code.trim());
270
+
271
+ const checkboxLiteral = literals.find((t) => ESCAPED_CHECKBOX_RE.test(t) && PHASE_WORD_RE.test(t));
272
+ if (checkboxLiteral !== undefined) report(CLASS.PHASE_ENTRY_CHECKBOX, checkboxLiteral);
273
+ }
274
+ return out;
275
+ }
276
+
277
+ const toPosix = (rel) => rel.split(path.sep).join('/');
278
+
279
+ function scanRepo(root) {
280
+ return scanTree({
281
+ root,
282
+ scanDirs: SCAN_DIRS,
283
+ scanExt: SCAN_EXT,
284
+ onFile(rel, text) {
285
+ const posix = toPosix(rel);
286
+ return findProgressDrift(text, posix).map((d) => ({ file: posix, ...d }));
287
+ },
288
+ });
289
+ }
290
+
291
+ // ---------------------------------------------------------------------------
292
+ // Positive control. One fixture per class that MUST be flagged, one per class
293
+ // that must NOT, and the owner-exemption boundary. The fixtures are source text
294
+ // handed to the same detector; nothing here touches the tree.
295
+ // ---------------------------------------------------------------------------
296
+ const FIXTURE_PATH = 'src/fixture-violation.cts';
297
+
298
+ const MUST_FLAG = [
299
+ { cls: CLASS.PROGRESS_HEADING, text: 'const m = content.match(/^##[ \\t]+Progress\\b/im);' },
300
+ { cls: CLASS.PROGRESS_HEADING, text: 'const re = new RegExp(`^#{1,3}\\\\s*Progress`);' },
301
+ { cls: CLASS.PROGRESS_HEADING, text: 'const m = /^##\\s+(Progress)/m.exec(content);' },
302
+ { cls: CLASS.PROGRESS_HEADING, text: 'const pct = done / total, re = /^##\\s+Progress/m;' },
303
+ { cls: CLASS.PROGRESS_HEADING, text: '/* gate */ const re = /^##\\s+Progress/m;' },
304
+ { cls: CLASS.PROGRESS_TITLE, text: 'const isProgress = (t) => /^Progress\\b/i.test(t);' },
305
+ { cls: CLASS.PROGRESS_TITLE, text: "const SRC = 'Progress\\\\b';" },
306
+ { cls: CLASS.PROGRESS_TABLE_REPLACE, text: 'const out = roadmapContent.replace(progressRowRe, row);' },
307
+ { cls: CLASS.PROGRESS_TABLE_REPLACE, text: 'const out = rm.replace(PROGRESS_TABLE_RE, row);' },
308
+ { cls: CLASS.PROGRESS_TABLE_REPLACE, text: 'const out = roadmap.replaceAll(/\\|\\s*3\\.\\s*Three\\s*\\|/g, "| 3. Three |");' },
309
+ { cls: CLASS.PROGRESS_TABLE_REPLACE, text: 'const out = text.replace(/\\|\\s*Plans Complete\\s*\\|/, "| Plans |");' },
310
+ { cls: CLASS.PHASE_ENTRY_CHECKBOX, text: 'const re = /^- \\[ \\] \\*\\*Phase (\\d+)/m;' },
311
+ { cls: CLASS.PHASE_ENTRY_CHECKBOX, text: 'const re = /^\\s*-\\s*\\[ ?[xX]? ?\\]\\s*\\*\\*Phase/;' },
312
+ { cls: CLASS.PHASE_ENTRY_CHECKBOX, text: 'const re = /^- \\[.\\] \\*\\*Phase/;' },
313
+ { cls: CLASS.PHASE_ENTRY_CHECKBOX, text: 'const re = /^-\\s\\[\\s\\]\\sPhase/;' },
314
+ { cls: CLASS.PHASE_ENTRY_CHECKBOX, text: 'const re = new RegExp(`-\\\\s*\\\\[[ xX]\\\\]\\\\s*\\\\*\\\\*Phase ${n}`);' },
315
+ ];
316
+
317
+ const MUST_STAY_CLEAN = [
318
+ // A heading being WRITTEN: no regex syntax.
319
+ { name: 'heading builder', text: "const heading = '## Progress\\n\\n';" },
320
+ { name: 'bold heading builder', text: "const heading = '## **Progress**\\n';" },
321
+ // STATE.md field, status vocabulary.
322
+ { name: 'STATE progress field', text: 'const m = /^Progress: (.*)$/m.exec(body);' },
323
+ { name: 'status vocabulary', text: 'const m = content.match(/##\\s*Status:\\s*(OPEN|IN PROGRESS)/i);' },
324
+ { name: 'status alternation', text: 'const m = /^(done|progress\\b)/i.exec(word);' },
325
+ // Other checkboxes and a different seam: the heading bracket tag, a markdown link.
326
+ { name: 'requirement checkbox', text: 'const re = /^- \\[[ xX]\\] \\*\\*REQ-\\d+\\*\\*/m;' },
327
+ { name: 'hyphenated Phase', text: 'const re = /^- \\[[ x]\\] .*Phase-gate/;' },
328
+ { name: 'emitted entry', text: 'const line = `- [ ] **Phase ${n}: ${name}**`;' },
329
+ { name: 'heading bracket tag', text: 'const re = /^#{2,4}\\s*(?:\\[[^\\]]{1,200}\\]\\s*)?Phase\\s+(\\d+)/;' },
330
+ { name: 'markdown link', text: 'const re = /\\[Phase (\\d+)\\]\\(#phase-\\1\\)/;' },
331
+ { name: 'comment', text: '// the old /^##[ \\t]+Progress\\b/im and /^- \\[ \\] \\*\\*Phase/ shapes' },
332
+ // A replace whose first argument is not a Progress table pattern.
333
+ { name: 'unrelated replace', text: 'const out = roadmapContent.replace(/\\r\\n/g, "\\n");' },
334
+ { name: 'pipe escape for output', text: "const cell = roadmap.title.replace(/\\|/g, '\\\\|');" },
335
+ { name: 'progress variable receiver', text: "const pct = roadmapProgress.replace(/%$/, '');" },
336
+ { name: 'replace inside a template', text: 'const label = `roadmap progress ${name.replace(/-/g, " ")}`;' },
337
+ ];
338
+
339
+ /**
340
+ * Run the positive and negative controls. Returns a list of human-readable
341
+ * failures; empty means the detector can go red where it must and stays quiet where
342
+ * it must.
343
+ */
344
+ function runPositiveControl() {
345
+ const failures = [];
346
+ for (const fixture of MUST_FLAG) {
347
+ const found = findProgressDrift(fixture.text, FIXTURE_PATH).filter((d) => d.cls === fixture.cls);
348
+ if (found.length === 0) failures.push(`not flagged (${fixture.cls}): ${fixture.text}`);
349
+ }
350
+ for (const fixture of MUST_STAY_CLEAN) {
351
+ const found = findProgressDrift(fixture.text, FIXTURE_PATH);
352
+ if (found.length > 0) failures.push(`flagged but clean (${fixture.name}): ${found.map((d) => d.cls).join(',')}`);
353
+ }
354
+ // Owner boundary: each owner may spell its own classes and nothing else.
355
+ const progressSpelling = MUST_FLAG.find((f) => f.cls === CLASS.PROGRESS_HEADING).text;
356
+ const checkboxSpelling = MUST_FLAG.find((f) => f.cls === CLASS.PHASE_ENTRY_CHECKBOX).text;
357
+ const boundary = [
358
+ [PROGRESS_OWNER, progressSpelling, 0, 'progress owner may spell a Progress heading'],
359
+ [PROGRESS_OWNER, checkboxSpelling, 1, 'progress owner may NOT spell a phase-entry checkbox'],
360
+ [GRAMMAR_OWNER, checkboxSpelling, 0, 'grammar owner may spell a phase-entry checkbox'],
361
+ [GRAMMAR_OWNER, progressSpelling, 1, 'grammar owner may NOT spell a Progress heading'],
362
+ ];
363
+ for (const [rel, text, expected, label] of boundary) {
364
+ const count = findProgressDrift(text, rel).length;
365
+ if (count !== expected) failures.push(`owner boundary (${label}): expected ${expected}, got ${count}`);
366
+ }
367
+ return failures;
368
+ }
369
+
370
+ /**
371
+ * The failure report as lines: a header, the pointer to the two owners, then one
372
+ * `file:line [class] fragment` line per violation. `found` is the matched
373
+ * literal (or, for the replace class, the offending line), cut to
374
+ * MAX_REGEX_LITERAL_LEN. The path is as attacker-controlled as the matched text,
375
+ * so both pass through `sanitizeForReport`.
376
+ */
377
+ function formatViolations(violations) {
378
+ return [
379
+ 'roadmap-progress-drift: hand-written Progress-table or phase-entry pattern(s) found.',
380
+ 'Locate and edit the Progress table through src/roadmap-progress.cts (`locateRoadmapProgress`, `findProgressSection`,',
381
+ '`editRoadmapProgress`) and compose phase-entry patterns from src/phase-entry-grammar.cts (`phaseEntryRegExp`,',
382
+ '`phaseEntrySplitRegExp`) instead of spelling them again:',
383
+ ...violations.map((d) => ` ${sanitizeForReport(d.file)}:${d.line} [${d.cls}] ${sanitizeForReport(d.found)}`),
384
+ ];
385
+ }
386
+
387
+ function main() {
388
+ const controlFailures = runPositiveControl();
389
+ if (controlFailures.length > 0) {
390
+ process.stderr.write('roadmap-progress-drift: the guard failed its own positive control; a detector that cannot go red is not a guard.\n');
391
+ for (const f of controlFailures) process.stderr.write(` ${f}\n`);
392
+ process.exitCode = 1;
393
+ return;
394
+ }
395
+ const root = path.join(__dirname, '..');
396
+ const violations = scanRepo(root);
397
+ if (violations.length === 0) {
398
+ process.stdout.write('ok roadmap-progress-drift: no hand-written Progress-table or phase-entry pattern outside roadmap-progress.cts and phase-entry-grammar.cts (positive control passed)\n');
399
+ return;
400
+ }
401
+ for (const line of formatViolations(violations)) process.stderr.write(`${line}\n`);
402
+ process.exitCode = 1;
403
+ }
404
+
405
+ if (require.main === module) main();
406
+
407
+ module.exports = {
408
+ findProgressDrift,
409
+ scanRepo,
410
+ runPositiveControl,
411
+ formatViolations,
412
+ CLASS,
413
+ PROGRESS_OWNER,
414
+ GRAMMAR_OWNER,
415
+ OWNED_CLASSES,
416
+ SCAN_DIRS,
417
+ MUST_FLAG,
418
+ MUST_STAY_CLEAN,
419
+ stripComments,
420
+ literalsOn,
421
+ };
@@ -657,7 +657,29 @@ const COVERED = {
657
657
  'state-contract': {
658
658
  cjs: 'gsd-core/bin/lib/state-contract.cjs',
659
659
  excludeTests: ['state-contract.test.cjs'],
660
- minScore: 65,
660
+ // #5764: CI run 40800623930, job 123207881000, `Stryker (state-contract)`: measured 70.43%
661
+ // once the checklist fallback ran through the phase-entry grammar; floor(70.43)-1.
662
+ minScore: 69,
663
+ },
664
+ // #4998 review (trek-e 5563202317 Nit 6, 5608265301 Minor 4): the STATE.md and
665
+ // planning-document field readers and writers #4998 grew.
666
+ // state-document: CI run 39742269013 (job 119702274448) measured 50.47% in a 94-minute
667
+ // job. floor(50.47) - 1 = 49 is below the range minimum, so minScore stays at 50.
668
+ // 140 is about 1.5x the measured job time.
669
+ 'state-document': {
670
+ cjs: 'gsd-core/bin/lib/state-document.cjs',
671
+ minScore: 50,
672
+ timeoutMinutes: 140,
673
+ },
674
+ // planning-document.test.cjs requires the module through a MODULE_PATH constant, which
675
+ // the require derivation cannot follow; frontmatter-fence.test.cjs requires it directly
676
+ // but sits outside the naming rule. Both run in-process, with no subprocess.
677
+ 'planning-document': {
678
+ cjs: 'gsd-core/bin/lib/planning-document.cjs',
679
+ extraTests: ['frontmatter-fence.test.cjs', 'planning-document.test.cjs'],
680
+ // CI run 39736529871, job 119683571231 (#5234, head 43f1bfa93): 75.14%, Stryker in
681
+ // about 3 minutes. Floor = floor(75.14) - 1 = 74.
682
+ minScore: 74,
661
683
  },
662
684
  };
663
685
 
@@ -252,6 +252,39 @@ function runInstalledTreeAudit(cwd, opts = {}) {
252
252
  return runNpmAuditWithRetry(cwd, ['audit', '--omit=dev', '--json'], opts);
253
253
  }
254
254
 
255
+ /**
256
+ * `-c safe.directory=<repoRoot>` for the baseline's git calls (#5659). On the gsd-test bench the
257
+ * checkout is owned by a different uid than the one running the tests, so git refuses every command
258
+ * ("detected dubious ownership") and the baseline silently resolved to nothing, which sent the
259
+ * integrity tests to their zero-tolerance fallback. git honors `safe.directory` from the command line
260
+ * only; scoping it to the audited repo root (never `*`, never global config) trusts exactly the
261
+ * checkout being audited. Same precedent as scripts/lint-compiled-artifact-sync.cjs.
262
+ */
263
+ function safeDirectoryArgs(repoRoot) {
264
+ // git compares safe.directory against its forward-slash path, also on Windows.
265
+ return ['-c', `safe.directory=${String(repoRoot).replace(/\\/g, '/')}`];
266
+ }
267
+
268
+ /**
269
+ * One line saying why no baseline resolved (#5659), for the integrity gate's zero-tolerance message.
270
+ * Without it a git refusal (ownership, a missing git, a broken checkout) is indistinguishable from a
271
+ * checkout that simply has no `origin/next`, which is how the ownership problem hid. Only the first
272
+ * line git printed is kept.
273
+ */
274
+ function explainNoBaseline(repoRoot, { execFileSyncImpl = execFileSync } = {}) {
275
+ try {
276
+ execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'rev-parse', '--git-dir'], {
277
+ cwd: repoRoot,
278
+ encoding: 'utf-8',
279
+ stdio: ['ignore', 'pipe', 'pipe'],
280
+ });
281
+ } catch (error) {
282
+ const detail = String((error && (error.stderr || error.message)) || '').trim().split(/\r?\n/)[0];
283
+ return `git could not read ${repoRoot}${detail ? `: ${detail}` : ''}`;
284
+ }
285
+ return 'no baseline ref (origin/next, next) resolved, or it lacks package.json / package-lock.json';
286
+ }
287
+
255
288
  /**
256
289
  * Extracts package.json + package-lock.json (optionally under `subdir`,
257
290
  * e.g. 'sdk') from `ref` at git object level into a fresh temp directory --
@@ -260,17 +293,17 @@ function runInstalledTreeAudit(cwd, opts = {}) {
260
293
  * clone that never fetched it) or either file is absent at that ref --
261
294
  * callers treat `null` as "no baseline available", not an error.
262
295
  */
263
- function extractBaselineTree(ref, repoRoot, subdir = '') {
296
+ function extractBaselineTree(ref, repoRoot, subdir = '', { execFileSyncImpl = execFileSync } = {}) {
264
297
  const rel = (name) => (subdir ? path.posix.join(subdir, name) : name);
265
298
  let pkgJson;
266
299
  let lockJson;
267
300
  try {
268
- pkgJson = execFileSync('git', ['show', `${ref}:${rel('package.json')}`], {
301
+ pkgJson = execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'show', `${ref}:${rel('package.json')}`], {
269
302
  cwd: repoRoot,
270
303
  encoding: 'utf-8',
271
304
  stdio: ['ignore', 'pipe', 'ignore'],
272
305
  });
273
- lockJson = execFileSync('git', ['show', `${ref}:${rel('package-lock.json')}`], {
306
+ lockJson = execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'show', `${ref}:${rel('package-lock.json')}`], {
274
307
  cwd: repoRoot,
275
308
  encoding: 'utf-8',
276
309
  stdio: ['ignore', 'pipe', 'ignore'],
@@ -322,12 +355,12 @@ const NULL_SHA = '0000000000000000000000000000000000000000';
322
355
  * Returns '' if none resolve -- callers fall back to strict zero-tolerance
323
356
  * rather than silently skipping the gate.
324
357
  */
325
- function resolveBaselineRef(repoRoot) {
358
+ function resolveBaselineRef(repoRoot, { execFileSyncImpl = execFileSync } = {}) {
326
359
  if (process.env.AUDIT_BASELINE_REF) return process.env.AUDIT_BASELINE_REF;
327
360
  if (process.env.GITHUB_BASE_REF) return `origin/${process.env.GITHUB_BASE_REF}`;
328
361
  if (process.env.GITHUB_EVENT_NAME === 'push') {
329
362
  try {
330
- const parent = execFileSync('git', ['rev-parse', '--verify', 'HEAD~1'], {
363
+ const parent = execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'rev-parse', '--verify', 'HEAD~1'], {
331
364
  cwd: repoRoot,
332
365
  encoding: 'utf-8',
333
366
  stdio: ['ignore', 'pipe', 'ignore'],
@@ -338,7 +371,7 @@ function resolveBaselineRef(repoRoot) {
338
371
  }
339
372
  }
340
373
  try {
341
- execFileSync('git', ['rev-parse', '--verify', 'origin/next'], {
374
+ execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'rev-parse', '--verify', 'origin/next'], {
342
375
  cwd: repoRoot,
343
376
  stdio: 'ignore',
344
377
  });
@@ -347,7 +380,7 @@ function resolveBaselineRef(repoRoot) {
347
380
  // fall through to a plain local branch
348
381
  }
349
382
  try {
350
- execFileSync('git', ['rev-parse', '--verify', 'next'], {
383
+ execFileSyncImpl('git', [...safeDirectoryArgs(repoRoot), 'rev-parse', '--verify', 'next'], {
351
384
  cwd: repoRoot,
352
385
  stdio: 'ignore',
353
386
  });
@@ -366,6 +399,7 @@ module.exports = {
366
399
  runNpmAuditWithRetry,
367
400
  extractBaselineTree,
368
401
  resolveBaselineRef,
402
+ explainNoBaseline,
369
403
  isTimeoutKill,
370
404
  buildTimeoutKillError,
371
405
  sleepSyncMs,