@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
@@ -24,9 +24,6 @@ const { collectSection, tokenizeHeadings } = markdownSectionizer;
24
24
  const markdownTable = require("./markdown-table.cjs");
25
25
  const { splitTableRow, isDelimiterRow } = markdownTable;
26
26
  // eslint-disable-next-line @typescript-eslint/no-require-imports
27
- const roadmapParser = require("./roadmap-parser.cjs");
28
- const { getMilestonePhaseFilter } = roadmapParser;
29
- // eslint-disable-next-line @typescript-eslint/no-require-imports
30
27
  const coreUtils = require("./core-utils.cjs");
31
28
  const { toPosixPath } = coreUtils;
32
29
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -37,10 +34,10 @@ const frontmatter = require("./frontmatter.cjs");
37
34
  const { extractFrontmatter } = frontmatter;
38
35
  // eslint-disable-next-line @typescript-eslint/no-require-imports
39
36
  const phaseIdMod = require("./phase-id.cjs");
40
- const { PHASE_NUMBER_TOKEN_SOURCE } = phaseIdMod;
37
+ const { PHASE_NUMBER_TOKEN_SOURCE, scopeToPhase } = phaseIdMod;
41
38
  // eslint-disable-next-line @typescript-eslint/no-require-imports
42
39
  const phaseLocator = require("./phase-locator.cjs");
43
- const { getArchivedPhaseDirs } = phaseLocator;
40
+ const { getArchivedPhaseDirs, listMilestonePhaseDirs } = phaseLocator;
44
41
  const security_cjs_1 = require("./security.cjs");
45
42
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
46
43
  const configLoader = require("./config-loader.cjs");
@@ -65,19 +62,18 @@ function cmdAuditUat(cwd, raw) {
65
62
  if (!hasActivePhases && archivedDirs.length === 0) {
66
63
  error('No phases directory found in planning directory');
67
64
  }
68
- const isDirInMilestone = getMilestonePhaseFilter(cwd);
69
65
  const results = [];
70
66
  // Active dirs are milestone-filtered; archived dirs deliberately are NOT.
71
- // getMilestonePhaseFilter derives the CURRENT milestone's phase numbers from
72
- // ROADMAP.md, and archived phases belong to past milestones by definition — so
73
- // applying it to them discards every one and silently reinstates the bug.
67
+ // listMilestonePhaseDirs derives the CURRENT milestone's phase directories
68
+ // (window + sentinel filtered) from ROADMAP.md, and archived phases belong
69
+ // to past milestones by definition — so applying it to them discards every
70
+ // one and silently reinstates the bug.
74
71
  const scanTargets = [];
75
72
  if (hasActivePhases) {
76
- const dirs = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true })
77
- .filter(e => e.isDirectory())
78
- .map(e => e.name)
79
- .filter(isDirInMilestone)
80
- .sort();
73
+ // #3185 (ADR-3180 Decision 1): routed through the canonical owner
74
+ // instead of a hand-rolled readdirSync + isDirInMilestone filter, which
75
+ // also never excluded sentinels, unlike the owner.
76
+ const dirs = listMilestonePhaseDirs(phasesDir, { cwd }).value;
81
77
  for (const dir of dirs) {
82
78
  scanTargets.push({ dir, phaseDir: node_path_1.default.join(phasesDir, dir) });
83
79
  }
@@ -93,8 +89,12 @@ function cmdAuditUat(cwd, raw) {
93
89
  const phaseMatch = dir.match(new RegExp(`^(${PHASE_NUMBER_TOKEN_SOURCE})`, 'i'));
94
90
  const phaseNum = phaseMatch ? phaseMatch[1] : dir;
95
91
  const files = node_fs_1.default.readdirSync(phaseDir);
96
- // Process UAT files
97
- for (const file of files.filter(f => f.includes('-UAT') && f.endsWith('.md'))) {
92
+ // Process UAT files — scoped to THIS phase's own token (#3511) via
93
+ // scopeToPhase, so a stray, cross-phase, or ad-hoc file cannot be reported
94
+ // under this phase's audit-uat entry. A phase whose own UAT file is
95
+ // genuinely absent scopes to empty and contributes nothing — correct, and
96
+ // the reason scopeToPhase has no unfiltered fallback.
97
+ for (const file of scopeToPhase(files.filter(f => f.includes('-UAT') && f.endsWith('.md')), dir)) {
98
98
  const uatFilePath = node_path_1.default.join(phaseDir, file);
99
99
  const content = node_fs_1.default.readFileSync(uatFilePath, 'utf-8');
100
100
  const items = parseUatItems(content);
@@ -111,8 +111,9 @@ function cmdAuditUat(cwd, raw) {
111
111
  });
112
112
  }
113
113
  }
114
- // Process VERIFICATION files
115
- for (const file of files.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md'))) {
114
+ // Process VERIFICATION files — scoped to THIS phase's own token (#3511)
115
+ // for the same reason as the UAT loop above.
116
+ for (const file of scopeToPhase(files.filter(f => f.includes('-VERIFICATION') && f.endsWith('.md')), dir)) {
116
117
  const verificationFilePath = node_path_1.default.join(phaseDir, file);
117
118
  const content = node_fs_1.default.readFileSync(verificationFilePath, 'utf-8');
118
119
  const status = extractFrontmatter(content, verificationFilePath).status || 'unknown';
@@ -766,31 +767,308 @@ function parseGapsTableItems(sectionBody) {
766
767
  * `.planning/todos/pending/*.md` entry required). Every other entry —
767
768
  * including one with no `status:` field at all — is UNRESOLVED and is
768
769
  * surfaced.
770
+ *
771
+ * #3457: when the section body contains headings, entries are delimited by
772
+ * LEAF headings (see `splitDeferredHeadingEntries`) rather than by bullets —
773
+ * the executor convention writes one deferred item as a heading followed by
774
+ * sibling `- **Field:** …` bullets, which the bullet-only split mis-counted as
775
+ * one item PER BULLET. A body with no headings keeps the original
776
+ * one-bullet-per-item split unchanged.
769
777
  */
770
- function parseDeferredItems(content) {
778
+ /**
779
+ * One `deferred-items.md` entry with its RAW (un-lowercased) `status:` field
780
+ * value (`''` when the entry carries no parseable status). #3458 follow-up:
781
+ * `parseDeferredItems` (below) is now DEFINED IN TERMS OF this — it filters
782
+ * to `status !== 'resolved'` — and `audit.cts`'s `scanDeferredItems` also
783
+ * consumes this directly so it can tell `resolved` (fixed for real, never
784
+ * counted), the newer `acknowledged` (suppressed-but-tallied, #3458
785
+ * follow-up), and everything else (open) apart WITHOUT a second,
786
+ * independent entry-boundary/field-extraction pass that could drift from
787
+ * this one.
788
+ */
789
+ function parseDeferredItemsWithStatus(content) {
771
790
  const deferredSection = collectSection(content, (h) => /^deferred\s+items$/i.test(h.text) && h.level === 2, { levelBounded: true });
772
791
  const sectionBody = deferredSection ? deferredSection.body : content;
773
792
  const items = [];
774
- for (const entryLines of splitGapsEntries(sectionBody)) {
775
- const fields = extractGapEntryFields(entryLines);
776
- const rawStatus = fields.status;
777
- if (rawStatus && rawStatus.toLowerCase() === 'resolved')
778
- continue;
793
+ // #3457: heading-delimited shape — an entry's fields live in sibling bullets
794
+ // (`- **Status:** resolved`), so the bullet marker is stripped on EVERY line
795
+ // before field extraction, not just line 0 (which `extractGapEntryFields`
796
+ // does for the headless/Gaps shape, where a later `- ` line is a nested
797
+ // sub-list, not a field).
798
+ const headingEntries = splitDeferredHeadingEntries(sectionBody);
799
+ const entries = headingEntries !== null
800
+ ? headingEntries.map((entryLines) => ({
801
+ lines: entryLines,
802
+ fields: extractGapEntryFields(entryLines.map(stripLeadingBulletMarker)),
803
+ }))
804
+ : splitGapsEntries(sectionBody).map((entryLines) => ({
805
+ lines: entryLines,
806
+ fields: extractGapEntryFields(entryLines),
807
+ }));
808
+ for (const { lines: entryLines, fields } of entries) {
779
809
  const text = rawGapEntryText(entryLines);
780
810
  if (!text)
781
811
  continue;
782
- items.push({
783
- name: text,
784
- result: 'unresolved',
785
- category: 'deferred',
786
- });
812
+ items.push({ name: text, status: fields.status || '' });
787
813
  }
788
814
  // #2766: union with the table form — see parseDeferredTableItems. Executors
789
815
  // write this file by hand with no mandated shape, and a GFM table is a natural
790
816
  // choice for the common "test → failing seeds" case, which produced ZERO items.
791
- items.push(...parseDeferredTableItems(sectionBody));
817
+ // Table rows carry no independently-parseable status column in general —
818
+ // `parseDeferredTableItems` already excludes resolved/done/pass rows at its
819
+ // own layer (any cell reading exactly one of those three) — so anything it
820
+ // returns here is inherently open; `acknowledge` (#3458 follow-up) has no
821
+ // representable field to write for a table row, so those are reported with
822
+ // status `''` (never `resolved`/`acknowledged`) and remain permanently
823
+ // un-acknowledgeable via the CLI writer — a known, deliberate limitation
824
+ // (see `acknowledgeDeferredItem`'s doc comment).
825
+ items.push(...parseDeferredTableItems(sectionBody).map((item) => ({ name: item.name, status: '' })));
792
826
  return items;
793
827
  }
828
+ function parseDeferredItems(content) {
829
+ return parseDeferredItemsWithStatus(content)
830
+ .filter((entry) => !(entry.status && entry.status.toLowerCase() === 'resolved'))
831
+ .map((entry) => ({
832
+ name: entry.name,
833
+ result: 'unresolved',
834
+ category: 'deferred',
835
+ }));
836
+ }
837
+ /**
838
+ * CLI-writer half of the #3458 follow-up deferred_items suppression seam.
839
+ * Sets the ONE deferred entry whose rendered text (`rawGapEntryText`, the
840
+ * same value `parseDeferredItemsWithStatus`/the audit's JSON output surface
841
+ * as `name`/`text`) exactly equals `targetText` to `status: acknowledged` —
842
+ * a NEW terminal value, distinct from the existing `resolved` (which keeps
843
+ * meaning "actually fixed"). This is the marker for this category: unlike
844
+ * every other audit category (a sibling `audit_acknowledged` frontmatter map
845
+ * that never touches the artifact's own `status:`), a deferred-items.md
846
+ * entry's `status:` field carries no OTHER meaning, so the field itself
847
+ * doubles as the marker — self-invalidating for free: edit the entry's
848
+ * `status:` away from `acknowledged` (or delete the field) and it resurfaces
849
+ * with no separate cleanup step, exactly like every other category's marker.
850
+ *
851
+ * Deliberately refuses (`unsupported_heading_shape`) rather than guess when
852
+ * the section uses the heading-delimited (#3457) entry shape: reliably
853
+ * mapping a `splitDeferredHeadingEntries` entry back to its EXACT source line
854
+ * span is not safely derivable without re-deriving that function's
855
+ * leaf/container walk against a document that may also mix in headless
856
+ * (`splitGapsEntries`-derived) entries between headings — attempting it risks
857
+ * writing into the WRONG entry. The bullet-only (headless) shape below is the
858
+ * primary, documented SCOPE BOUNDARY convention and is handled precisely.
859
+ *
860
+ * Also refuses `ambiguous` (2+ entries share the exact same text — status must
861
+ * be unique to identify one) and `not_found`, and is a no-op
862
+ * (`already_resolved`) on an entry already carrying `status: resolved` — the
863
+ * verdict-preserving direction: acknowledging a genuinely-fixed item would
864
+ * silently downgrade its terminal state.
865
+ *
866
+ * SPAN-CARRIED, not re-searched (F1, #3458 follow-up review — see
867
+ * `splitGapsEntriesWithSpans`'s doc comment): the target entry's location
868
+ * within `sectionBody` is the (start, end) character span recorded by
869
+ * `splitGapsEntriesWithSpans` in the SAME pass that produced `entryLines` /
870
+ * `targetText` above — never re-derived afterwards by searching. The
871
+ * previous implementation re-found the entry with a regex anchored on its
872
+ * own (escaped) exact text; that regex necessarily matches the FIRST
873
+ * occurrence of that text within `sectionBody`, which is not always the
874
+ * entry that was actually selected (a continuation/quoted line inside an
875
+ * EARLIER or LATER entry can carry byte-identical text) — and because the
876
+ * mis-targeted span is byte-identical to `targetText`, no downstream check
877
+ * on the WRITTEN text could ever distinguish a wrong-entry write from a
878
+ * correct one. Carrying the span removes the re-derivation step entirely:
879
+ * there is no second search to mis-target.
880
+ *
881
+ * Section-anchored (BLOCKER 1, #3458 follow-up review): the span is
882
+ * `sectionBody`-relative — the SAME string `matches`/the `ambiguous` guard
883
+ * were computed over — not `content`-relative, so an identical bullet living
884
+ * outside `## Deferred Items` (e.g. in an unrelated `# Notes` or a
885
+ * UAT/VERIFICATION body) can never steal the write. The span is translated
886
+ * into `content`-relative offsets via `deferredSection.bodyStart` (the
887
+ * section's own start offset, an invariant `collectSection` guarantees:
888
+ * `content.slice(bodyStart, bodyEnd) === body`). Before writing, the
889
+ * spanned text's own raw entry is re-derived and compared against
890
+ * `targetText` one more time — this is now a GENUINE invariant check (the
891
+ * span was computed by `splitGapsEntriesCore`'s independent offset
892
+ * bookkeeping, a different code path than the `entryLines`/`targetText`
893
+ * comparison above), not a no-op — if it does not match, the write is
894
+ * refused with `match_verification_failed` rather than risk touching the
895
+ * wrong span.
896
+ */
897
+ function acknowledgeDeferredItem(content, targetText) {
898
+ const deferredSection = collectSection(content, (h) => /^deferred\s+items$/i.test(h.text) && h.level === 2, { levelBounded: true });
899
+ const sectionBody = deferredSection ? deferredSection.body : content;
900
+ if (splitDeferredHeadingEntries(sectionBody) !== null) {
901
+ return { content, status: 'unsupported_heading_shape' };
902
+ }
903
+ const entries = splitGapsEntriesWithSpans(sectionBody);
904
+ const matches = entries
905
+ .map((entry) => ({ entry, text: rawGapEntryText(entry.lines) }))
906
+ .filter((e) => e.text === targetText);
907
+ if (matches.length === 0)
908
+ return { content, status: 'not_found' };
909
+ if (matches.length > 1)
910
+ return { content, status: 'ambiguous' };
911
+ const { entry } = matches[0];
912
+ const { lines: entryLines, start, end } = entry;
913
+ const fields = extractGapEntryFields(entryLines);
914
+ if (fields.status && fields.status.toLowerCase() === 'resolved') {
915
+ return { content, status: 'already_resolved' };
916
+ }
917
+ // Anchor to the SAME section body `matches`/the `ambiguous` guard above
918
+ // were computed over (BLOCKER 1) — never the whole `content`, which could
919
+ // contain an identical bullet elsewhere. `start`/`end` are the entry's own
920
+ // span, carried directly from `splitGapsEntriesWithSpans` — no re-search.
921
+ const sectionOffset = deferredSection ? deferredSection.bodyStart : 0;
922
+ const matchedLines = sectionBody.slice(start, end).split('\n');
923
+ // Genuine invariant re-verification (see doc comment above): the span was
924
+ // computed by a code path independent of the `entryLines`/`targetText`
925
+ // comparison that selected this entry — this catches real drift between
926
+ // the two rather than a regex trivially guaranteed to agree with itself.
927
+ const strippedForVerify = matchedLines.map((l) => l.replace(/\r$/, ''));
928
+ if (rawGapEntryText(strippedForVerify) !== targetText) {
929
+ return { content, status: 'match_verification_failed' };
930
+ }
931
+ const matchIndexInContent = sectionOffset + start;
932
+ const statusFieldRe = /^\s*(?:-\s+)?(\*+status:\*+|status:)/i;
933
+ const statusLineIdx = matchedLines.findIndex((rawLine) => statusFieldRe.test(rawLine.replace(/\r$/, '')));
934
+ // No CRLF-preservation branch here (WARNING 1, #3458 follow-up review):
935
+ // every write goes through `platformWriteSync` → `normalizeContent`, which
936
+ // for a `.md` path unconditionally runs `_normalizeMd` — whole-file
937
+ // `\r\n` → `\n`, plus blank-line normalization around headings/lists — on
938
+ // EVERY write, not just this one. That is this codebase's single,
939
+ // deliberate OS-facing I/O seam (`shell-command-projection.cts`), applied
940
+ // uniformly to every `.md` writer; carving out one exception here would
941
+ // fight it rather than follow it, for a guarantee (byte-identical CRLF on
942
+ // disk) the seam already makes impossible. A marker write on a CRLF
943
+ // `deferred-items.md` normalizes the WHOLE file to LF, same as any other
944
+ // `.md` write in this codebase — expected, not a regression to guard
945
+ // against. Where a source line still carries a trailing `\r` (read from an
946
+ // on-disk CRLF document before normalization), `String.prototype.replace`
947
+ // consumes it as part of `.*$` and the replacement text does not
948
+ // reproduce it, so it is dropped here too — consistent with the eventual
949
+ // whole-file normalization rather than duplicating it.
950
+ let newMatchedLines;
951
+ if (statusLineIdx === -1) {
952
+ const bulletIndentMatch = matchedLines[0].match(/^(\s*)-\s+/);
953
+ const continuationIndent = ' '.repeat((bulletIndentMatch ? bulletIndentMatch[1].length : 0) + 2);
954
+ newMatchedLines = [
955
+ matchedLines[0],
956
+ `${continuationIndent}status: acknowledged`,
957
+ ...matchedLines.slice(1),
958
+ ];
959
+ }
960
+ else {
961
+ const original = matchedLines[statusLineIdx];
962
+ const replaced = original.replace(/^(\s*(?:-\s+)?)(\*+status:\*+|status:)(\s*).*$/i, (_m, indent, key, ws) => `${indent}${key}${ws}acknowledged`);
963
+ newMatchedLines = matchedLines.slice();
964
+ newMatchedLines[statusLineIdx] = replaced;
965
+ }
966
+ const newContent = content.slice(0, matchIndexInContent) + newMatchedLines.join('\n') + content.slice(matchIndexInContent + (end - start));
967
+ return { content: newContent, status: 'ok' };
968
+ }
969
+ /**
970
+ * Strip one leading `- ` bullet marker (#3457). Heading-delimited deferred
971
+ * entries carry their fields as sibling bullets; `extractGapEntryFields` only
972
+ * de-bullets line 0 (Gaps-protective — there, a later `- ` line is a nested
973
+ * sub-list), so the deferred heading path de-bullets every line itself before
974
+ * field extraction. Non-bullet lines pass through untouched.
975
+ */
976
+ function stripLeadingBulletMarker(line) {
977
+ return line.replace(/^(\s*)-\s+/, '');
978
+ }
979
+ /**
980
+ * Split a deferred-items section body into entries delimited by LEAF headings
981
+ * (#3457). Returns `null` when the body contains no heading at all — the
982
+ * caller then falls back to `splitGapsEntries`, keeping headless
983
+ * one-bullet-per-item files byte-for-byte on the pre-#3457 path.
984
+ *
985
+ * A heading is a CONTAINER (group/provenance/title label, contributes no
986
+ * entry) iff the NEXT heading is deeper — a deeper heading lives inside its
987
+ * span. Otherwise it is a LEAF: an entry boundary. This handles all three
988
+ * corpus shapes without hardcoding a depth: flat `#` title + `##` entries
989
+ * (title's next heading is deeper → container; each `##` followed by a
990
+ * same-or-shallower heading → leaf), a `##` container with `###` entries
991
+ * (container's next heading is deeper), and mixed-depth files where a
992
+ * childless `##` entry sits alongside a `##` group with `###` children — every
993
+ * childless heading is a leaf at whatever depth it is written. The shallower
994
+ * rules the issue reports as already tried (split on every heading; shallowest
995
+ * level; deepest level) each mis-count one of these shapes.
996
+ *
997
+ * A leaf entry is [heading text, ...body lines up to the next heading] and is
998
+ * kept only when its body (minus table lines) contains at least one `- `
999
+ * bullet:
1000
+ * - a prose-only or bare heading contributes nothing — "prose is not an item"
1001
+ * is this parser's pre-existing contract (see the `# Notes` case);
1002
+ * - a table-only body is left entirely to `parseDeferredTableItems`, which
1003
+ * unions over the same section body, so the heading cannot double-count the
1004
+ * table's rows.
1005
+ *
1006
+ * Lines before the first heading, and lines directly under a container heading
1007
+ * (before its first child), are split one-bullet-per-item by the unchanged
1008
+ * `splitGapsEntries` — headless parity, so loose bullets before a later
1009
+ * heading group (the mixed shape) stay one item each.
1010
+ */
1011
+ function splitDeferredHeadingEntries(sectionBody) {
1012
+ const headings = tokenizeHeadings(sectionBody);
1013
+ if (headings.length === 0)
1014
+ return null;
1015
+ const lines = sectionBody.split('\n');
1016
+ const headingByLine = new Map();
1017
+ for (let i = 0; i < headings.length; i++) {
1018
+ // Container iff the next heading is deeper (see doc comment). An empty
1019
+ // heading text (`##` alone) does not itself mean container — the flag is
1020
+ // carried explicitly so a bare LEAF heading still opens an entry.
1021
+ const isContainer = i + 1 < headings.length && headings[i + 1].level > headings[i].level;
1022
+ headingByLine.set(headings[i].line, { text: headings[i].text, isContainer });
1023
+ }
1024
+ const entries = [];
1025
+ let current = null; // accumulating a leaf heading's entry
1026
+ let pending = []; // preamble / container-heading body lines
1027
+ let currentHasBullet = false;
1028
+ const flushCurrent = () => {
1029
+ // Keep the leaf entry only when its body carries a bullet; the heading
1030
+ // text line itself (element 0) never counts as one.
1031
+ if (current !== null && currentHasBullet)
1032
+ entries.push(current);
1033
+ current = null;
1034
+ currentHasBullet = false;
1035
+ };
1036
+ const flushPending = () => {
1037
+ entries.push(...splitGapsEntries(pending.join('\n')));
1038
+ pending = [];
1039
+ };
1040
+ for (let i = 0; i < lines.length; i++) {
1041
+ const lineNo = i + 1;
1042
+ const heading = headingByLine.get(lineNo);
1043
+ if (heading !== undefined) {
1044
+ flushCurrent();
1045
+ // Headless-shaped region (preamble / container-direct bullets) ends at
1046
+ // ANY heading; flushing here keeps entries in document order even when
1047
+ // a container's direct bullets precede its first child entry.
1048
+ flushPending();
1049
+ if (!heading.isContainer) {
1050
+ // Leaf heading: open an entry with the heading text as line 0.
1051
+ current = [heading.text];
1052
+ currentHasBullet = false;
1053
+ }
1054
+ continue;
1055
+ }
1056
+ // Table lines belong to parseDeferredTableItems, never to a heading entry.
1057
+ if (/^\s*\|/.test(lines[i].replace(/\r$/, '')))
1058
+ continue;
1059
+ if (current !== null) {
1060
+ current.push(lines[i]);
1061
+ if (/^\s*-\s/.test(lines[i].replace(/\r$/, '')))
1062
+ currentHasBullet = true;
1063
+ }
1064
+ else {
1065
+ pending.push(lines[i]);
1066
+ }
1067
+ }
1068
+ flushCurrent();
1069
+ flushPending();
1070
+ return entries;
1071
+ }
794
1072
  /**
795
1073
  * Extract deferred entries from GFM pipe tables in a deferred-items.md body
796
1074
  * (#2766) — a UNION with the bullet scan in `parseDeferredItems`.
@@ -831,28 +1109,37 @@ function parseDeferredTableItems(sectionBody) {
831
1109
  return items;
832
1110
  }
833
1111
  /**
834
- * Split a `## Gaps` section body into per-entry line groups on TOP-LEVEL
835
- * `- ` bullet openers.
836
- *
837
- * The indentation of the FIRST bullet line encountered establishes the
838
- * "top-level" indent for the whole section; any subsequent `- `-opening line
839
- * at that same indent (or shallower) starts a NEW entry, while everything
840
- * more deeply indented — field continuation lines (` status: ...`) AND
841
- * nested sub-lists (` - src/foo.ts` under ` artifacts:`) — is folded into
842
- * the CURRENT entry. This keeps a `artifacts:`/`missing:` sub-list's `- `
843
- * items from being mis-split into spurious standalone entries (#2286 review
844
- * LOW finding).
845
- *
846
- * Lines before the first bullet (e.g. the `<!-- YAML format ... -->` comment
847
- * the template emits) are discarded. An empty/whitespace-only section body
848
- * (heading present, no bullets) returns `[]`.
1112
+ * Shared walk behind `splitGapsEntries` and `splitGapsEntriesWithSpans` — ONE
1113
+ * pass over `sectionBody` that both groups its lines into entries (see
1114
+ * `splitGapsEntries`'s doc comment for the grouping rule) AND records each
1115
+ * entry's (start, end) character offset within `sectionBody`. Extracted so
1116
+ * the two public shapes can never drift apart on what counts as an entry
1117
+ * boundary — a second, independently-written grouping pass is exactly how a
1118
+ * span-carrying sibling could disagree with the plain-lines version it is
1119
+ * supposed to be span-annotating.
849
1120
  */
850
- function splitGapsEntries(sectionBody) {
851
- const lines = sectionBody.split('\n');
1121
+ function splitGapsEntriesCore(sectionBody) {
1122
+ const rawLines = sectionBody.split('\n');
1123
+ const lineStarts = [];
1124
+ const lineEnds = [];
1125
+ let cursor = 0;
1126
+ for (const rawLine of rawLines) {
1127
+ lineStarts.push(cursor);
1128
+ cursor += rawLine.length;
1129
+ lineEnds.push(cursor);
1130
+ cursor += 1; // the '\n' separator — absent after the final line, but nothing reads past it
1131
+ }
852
1132
  const entries = [];
853
1133
  let current = null;
1134
+ let currentStartLine = -1;
1135
+ let currentEndLine = -1;
854
1136
  let baseIndent = null;
855
- for (const rawLine of lines) {
1137
+ const flush = () => {
1138
+ if (current !== null) {
1139
+ entries.push({ lines: current, start: lineStarts[currentStartLine], end: lineEnds[currentEndLine] });
1140
+ }
1141
+ };
1142
+ rawLines.forEach((rawLine, idx) => {
856
1143
  const line = rawLine.replace(/\r$/, '');
857
1144
  const bulletMatch = line.match(/^(\s*)-\s/);
858
1145
  if (bulletMatch) {
@@ -860,20 +1147,61 @@ function splitGapsEntries(sectionBody) {
860
1147
  if (baseIndent === null)
861
1148
  baseIndent = indent;
862
1149
  if (indent <= baseIndent) {
863
- if (current)
864
- entries.push(current);
1150
+ flush();
865
1151
  current = [line];
866
- continue;
1152
+ currentStartLine = idx;
1153
+ currentEndLine = idx;
1154
+ return;
867
1155
  }
868
1156
  }
869
- if (current)
1157
+ if (current !== null) {
870
1158
  current.push(line);
1159
+ currentEndLine = idx;
1160
+ }
871
1161
  // else: pre-first-bullet content (e.g. the template's HTML comment) — discarded.
872
- }
873
- if (current)
874
- entries.push(current);
1162
+ });
1163
+ flush();
875
1164
  return entries;
876
1165
  }
1166
+ /**
1167
+ * Split a `## Gaps` section body into per-entry line groups on TOP-LEVEL
1168
+ * `- ` bullet openers.
1169
+ *
1170
+ * The indentation of the FIRST bullet line encountered establishes the
1171
+ * "top-level" indent for the whole section; any subsequent `- `-opening line
1172
+ * at that same indent (or shallower) starts a NEW entry, while everything
1173
+ * more deeply indented — field continuation lines (` status: ...`) AND
1174
+ * nested sub-lists (` - src/foo.ts` under ` artifacts:`) — is folded into
1175
+ * the CURRENT entry. This keeps a `artifacts:`/`missing:` sub-list's `- `
1176
+ * items from being mis-split into spurious standalone entries (#2286 review
1177
+ * LOW finding).
1178
+ *
1179
+ * Lines before the first bullet (e.g. the `<!-- YAML format ... -->` comment
1180
+ * the template emits) are discarded. An empty/whitespace-only section body
1181
+ * (heading present, no bullets) returns `[]`.
1182
+ */
1183
+ function splitGapsEntries(sectionBody) {
1184
+ return splitGapsEntriesCore(sectionBody).map((entry) => entry.lines);
1185
+ }
1186
+ /**
1187
+ * Sibling of `splitGapsEntries` (F1, #3458 follow-up review) that ADDITIVELY
1188
+ * carries each entry's character span — every existing `splitGapsEntries`
1189
+ * caller (`parseGapsItems`, `parseDeferredItemsWithStatus`,
1190
+ * `splitDeferredHeadingEntries`'s `flushPending`) is unaffected and keeps
1191
+ * using the plain `lines`-only shape. `acknowledgeDeferredItem` is the one
1192
+ * caller that needs a span: it used to select an entry via `splitGapsEntries`
1193
+ * and then RE-FIND that entry's location with a fresh regex search over
1194
+ * `sectionBody` — matching the FIRST occurrence of the entry's exact text,
1195
+ * not necessarily the entry actually selected (a continuation/quoted line
1196
+ * inside a DIFFERENT entry can carry byte-identical text). Because the
1197
+ * mis-targeted span is byte-identical to the target text, no check on the
1198
+ * WRITTEN result could ever tell a wrong-entry write apart from a correct
1199
+ * one. Carrying the span out of THIS same pass — the one that already knows
1200
+ * exactly where the entry lives — removes the re-derivation step entirely.
1201
+ */
1202
+ function splitGapsEntriesWithSpans(sectionBody) {
1203
+ return splitGapsEntriesCore(sectionBody);
1204
+ }
877
1205
  /**
878
1206
  * Extract `key: value` fields from one Gaps entry's lines, anchored to the
879
1207
  * START of each (bullet-marker-stripped, trimmed) line — never scanning the
@@ -888,10 +1216,22 @@ function splitGapsEntries(sectionBody) {
888
1216
  * any nested sub-list content in the template's field ordering); later
889
1217
  * `key:`-shaped nested-list content is captured, if it parses as one, but
890
1218
  * never overrides an already-seen top-level field.
1219
+ *
1220
+ * #3457: markdown emphasis around the KEY (`**Status:** resolved` — the
1221
+ * deferred-items convention bolds every field, and a bolded resolution marker
1222
+ * previously failed this regex outright and surfaced as its own bogus
1223
+ * unresolved entry) is unwrapped before the match, still anchored at the
1224
+ * start of the line. The unwrapped key is lower-cased, because the bolded
1225
+ * convention form is Title-cased (`**Status:**`) while the field vocabulary
1226
+ * this module reads is lowercase (`status`) — the same normalization
1227
+ * `mapGapsHeader` already applies to table header cells. Bare (unbolded) keys
1228
+ * keep their literal case, and mid-line emphasis is untouched, preserving the
1229
+ * start-anchored decoy invariant above.
891
1230
  */
892
1231
  function extractGapEntryFields(entryLines) {
893
1232
  const fields = {};
894
1233
  const fieldLineRe = /^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/;
1234
+ const boldedKeyRe = /^\*+([A-Za-z_][A-Za-z0-9_-]*):\*+/;
895
1235
  entryLines.forEach((rawLine, idx) => {
896
1236
  const line = rawLine.replace(/\r$/, '');
897
1237
  // Strip ONLY the entry-opening bullet marker (idx 0); a bullet marker on
@@ -899,7 +1239,8 @@ function extractGapEntryFields(entryLines) {
899
1239
  // `splitGapsEntries` already folding it in — it is not itself a field
900
1240
  // line unless it independently matches `key: value` after stripping.
901
1241
  const bulletStripped = line.match(/^(\s*)-\s+(.*)$/);
902
- const content = idx === 0 && bulletStripped ? bulletStripped[2] : line.trim();
1242
+ const content = (idx === 0 && bulletStripped ? bulletStripped[2] : line.trim())
1243
+ .replace(boldedKeyRe, (_m, key) => `${key.toLowerCase()}:`);
903
1244
  const m = fieldLineRe.exec(content);
904
1245
  if (!m)
905
1246
  return;
@@ -1134,4 +1475,6 @@ module.exports = {
1134
1475
  resolveCheckpointFrame,
1135
1476
  checkpointBoxLine,
1136
1477
  parseDeferredItems,
1478
+ parseDeferredItemsWithStatus,
1479
+ acknowledgeDeferredItem,
1137
1480
  };