@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
@@ -25,8 +25,13 @@ exports.updateBullet = updateBullet;
25
25
  exports.updateHeading = updateHeading;
26
26
  exports.extractTaggedBlocks = extractTaggedBlocks;
27
27
  exports.stripTaggedBlocks = stripTaggedBlocks;
28
+ exports.editOutsideTaggedBlocks = editOutsideTaggedBlocks;
29
+ exports.maskFences = maskFences;
30
+ exports.editOutsideFences = editOutsideFences;
28
31
  exports.replaceSection = replaceSection;
29
32
  exports.withSection = withSection;
33
+ exports.headingSliceRange = headingSliceRange;
34
+ exports.editHeadingSlice = editHeadingSlice;
30
35
  exports.deleteSection = deleteSection;
31
36
  exports.extractXmlTagBodies = extractXmlTagBodies;
32
37
  const pattern_cjs_1 = require("./pattern.cjs");
@@ -850,6 +855,116 @@ function stripTaggedBlocks(content, tagName, allowAttributes = false, caseSensit
850
855
  return content;
851
856
  return content.replace(taggedBlockPattern(tagName, caseSensitive ? 'g' : 'gi', allowAttributes), '');
852
857
  }
858
+ /**
859
+ * Run `edit` over `content` with every `<tagName>…</tagName>` block hidden from
860
+ * it, then put the blocks back byte-for-byte (#5543). The blocks are swapped for
861
+ * one-line placeholder comments, so `edit` sees the rest of the document in its
862
+ * original order and cannot touch, or be captured by, a block's contents.
863
+ *
864
+ * Same hardened block pattern as `stripTaggedBlocks` (exact `<tag>` opener, so
865
+ * `<details open>` stays visible to `edit`). If `edit` returns a non-string, or
866
+ * drops a placeholder, the original `content` is returned unchanged rather than
867
+ * a document with a block missing.
868
+ *
869
+ * Nested blocks are NOT supported, exactly as for `extractTaggedBlocks` and
870
+ * `stripTaggedBlocks`: the first `</tag>` closes the block. Every ROADMAP reader
871
+ * that strips shipped milestones shares that grammar, so this writer sees the
872
+ * same blocks those readers do.
873
+ */
874
+ function editOutsideTaggedBlocks(content, tagName, edit, allowAttributes = false, caseSensitive = false) {
875
+ if (typeof content !== 'string' || content.length === 0)
876
+ return content;
877
+ if (typeof tagName !== 'string' || tagName.length === 0)
878
+ return edit(content);
879
+ const blocks = [];
880
+ let nonce = 0;
881
+ while (content.includes(`<!-- gsd-masked-${tagName}-${nonce}-`))
882
+ nonce += 1;
883
+ const placeholder = (i) => `<!-- gsd-masked-${tagName}-${nonce}-${i} -->`;
884
+ const masked = content.replace(taggedBlockPattern(tagName, caseSensitive ? 'g' : 'gi', allowAttributes), (block) => {
885
+ blocks.push(block);
886
+ return placeholder(blocks.length - 1);
887
+ });
888
+ if (blocks.length === 0)
889
+ return edit(content);
890
+ const edited = edit(masked);
891
+ if (typeof edited !== 'string')
892
+ return content;
893
+ let restored = edited;
894
+ for (let i = 0; i < blocks.length; i++) {
895
+ const marker = placeholder(i);
896
+ const at = restored.indexOf(marker);
897
+ if (at === -1 || restored.indexOf(marker, at + marker.length) !== -1)
898
+ return content;
899
+ restored = restored.slice(0, at) + blocks[i] + restored.slice(at + marker.length);
900
+ }
901
+ return restored;
902
+ }
903
+ /**
904
+ * Hide every fenced code block from a line-oriented edit. State-transition writers locate their
905
+ * target with `/^Field:.*$/m`-style patterns; those match a fenced EXAMPLE of the field as readily
906
+ * as the live line, so the example got the new value and the live line stayed stale. Masking each
907
+ * block to a single `<!-- gsd-masked-fence-… -->` line lets the same pattern see only live lines;
908
+ * `restore` splices the blocks back byte-for-byte (CRLF included — the original line text is kept).
909
+ * The fence grammar is `scanFencedBlocks`' (the one every reader here uses), and an unterminated
910
+ * fence masks to the end of the content, as `stripFencedCode` treats it.
911
+ */
912
+ function maskFences(content) {
913
+ const identity = { masked: content, restore: (edited) => edited };
914
+ if (typeof content !== 'string' || content.length === 0)
915
+ return identity;
916
+ const lines = content.split('\n');
917
+ const blocks = scanFencedBlocks(lines);
918
+ if (blocks.length === 0)
919
+ return identity;
920
+ let nonce = 0;
921
+ while (content.includes(`<!-- gsd-masked-fence-${nonce}-`))
922
+ nonce += 1;
923
+ const placeholder = (i) => `<!-- gsd-masked-fence-${nonce}-${i} -->`;
924
+ const kept = [];
925
+ const originals = [];
926
+ let cursor = 0;
927
+ for (const block of blocks) {
928
+ const end = block.closeLineIdx === -1 ? lines.length - 1 : block.closeLineIdx;
929
+ for (; cursor < block.openLineIdx; cursor++)
930
+ kept.push(lines[cursor]);
931
+ originals.push(lines.slice(block.openLineIdx, end + 1).join('\n'));
932
+ kept.push(placeholder(originals.length - 1));
933
+ cursor = end + 1;
934
+ }
935
+ for (; cursor < lines.length; cursor++)
936
+ kept.push(lines[cursor]);
937
+ return {
938
+ masked: kept.join('\n'),
939
+ restore(edited) {
940
+ if (typeof edited !== 'string')
941
+ return null;
942
+ let restored = edited;
943
+ for (let i = 0; i < originals.length; i++) {
944
+ const marker = placeholder(i);
945
+ const at = restored.indexOf(marker);
946
+ if (at === -1 || restored.indexOf(marker, at + marker.length) !== -1)
947
+ return null;
948
+ restored = restored.slice(0, at) + originals[i] + restored.slice(at + marker.length);
949
+ }
950
+ return restored;
951
+ },
952
+ };
953
+ }
954
+ /**
955
+ * Run `edit` over `content` with every fenced code block hidden from it (see `maskFences`).
956
+ * Content without a fence is edited as given. An edit that returns a non-string, or that loses a
957
+ * placeholder, returns the original content unchanged.
958
+ */
959
+ function editOutsideFences(content, edit) {
960
+ if (typeof content !== 'string' || content.length === 0)
961
+ return content;
962
+ const mask = maskFences(content);
963
+ const edited = edit(mask.masked);
964
+ if (typeof edited !== 'string')
965
+ return content;
966
+ return mask.restore(edited) ?? content;
967
+ }
853
968
  // ─── replaceSection ───────────────────────────────────────────────────────────
854
969
  /**
855
970
  * Splice `newBody` in place of a section's body and return the resulting
@@ -922,6 +1037,29 @@ function withSection(content, target, edit, opts = {}) {
922
1037
  return content;
923
1038
  return replaceSection(content, section, newBody);
924
1039
  }
1040
+ /**
1041
+ * Range of the slice that starts at `headings[index]` and runs to the next
1042
+ * heading of level <= `maxLevel` (fence-aware: `headings` comes from
1043
+ * `tokenizeHeadings`). Unlike `collectSection` it keeps the heading line and
1044
+ * every trailing byte, so a caller that edits the slice can splice it back
1045
+ * byte-exact (#5762).
1046
+ */
1047
+ function headingSliceRange(content, headings, index, maxLevel = 2) {
1048
+ const next = headings.slice(index + 1).find((h) => h.level <= maxLevel);
1049
+ return { start: headings[index].offset, end: next ? next.offset : content.length };
1050
+ }
1051
+ /**
1052
+ * Run `edit` on `content.slice(range.start, end)` and splice the result back,
1053
+ * where `end` excludes the single newline that precedes the next heading — the
1054
+ * slice the pre-owner writers cut with `search(/\n#{1,2}[ \t]/)` — so a row
1055
+ * inserted at the end of a table keeps the blank line before that heading.
1056
+ */
1057
+ function editHeadingSlice(content, range, edit) {
1058
+ const end = range.end < content.length && range.end > range.start && content[range.end - 1] === '\n'
1059
+ ? range.end - 1
1060
+ : range.end;
1061
+ return content.slice(0, range.start) + edit(content.slice(range.start, end)) + content.slice(end);
1062
+ }
925
1063
  // ─── deleteSection ────────────────────────────────────────────────────────────
926
1064
  /**
927
1065
  * Delete an entire section — the matching heading line ITSELF plus its body —
@@ -952,6 +1090,8 @@ function withSection(content, target, edit, opts = {}) {
952
1090
  * double-blank gap accumulates where the section used to sit. Content
953
1091
  * elsewhere in the document is never touched.
954
1092
  *
1093
+ * `endOffset` adds an optional caller-owned maximum boundary. It never widens
1094
+ * the heading-level range, and callers that omit it retain the legacy bytes.
955
1095
  * Returns `content` unchanged when no heading matches `headingPredicate`
956
1096
  * (bounded no-op, mirroring `withSection`'s miss behaviour).
957
1097
  */
@@ -990,7 +1130,13 @@ function deleteSection(content, headingPredicate, opts = {}) {
990
1130
  }
991
1131
  const eofOffset = acc;
992
1132
  const sectionStart = lineOffsets[target.line - 1]; // start of the target heading LINE itself
993
- const sectionEnd = stopLine <= lines.length ? lineOffsets[stopLine - 1] : eofOffset;
1133
+ const headingBoundedEnd = stopLine <= lines.length ? lineOffsets[stopLine - 1] : eofOffset;
1134
+ const explicitEnd = typeof opts.endOffset === 'number' && Number.isFinite(opts.endOffset)
1135
+ ? Math.max(0, Math.min(content.length, opts.endOffset))
1136
+ : headingBoundedEnd;
1137
+ const sectionEnd = Math.min(headingBoundedEnd, explicitEnd);
1138
+ if (sectionEnd <= sectionStart)
1139
+ return content;
994
1140
  const before = content.slice(0, sectionStart);
995
1141
  const after = content.slice(sectionEnd);
996
1142
  // Collapse a resulting blank-line seam to at most one blank line (2 newlines).
@@ -25,6 +25,7 @@ exports.escapeCell = escapeCell;
25
25
  exports.appendQuickTaskRow = appendQuickTaskRow;
26
26
  exports.migrateQuickTasksTable = migrateQuickTasksTable;
27
27
  exports.resetQuickTaskRows = resetQuickTaskRows;
28
+ exports.updateTraceabilityCell = updateTraceabilityCell;
28
29
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
29
30
  // ─── Schema registry ──────────────────────────────────────────────────────────
30
31
  /**
@@ -978,5 +979,27 @@ function resetQuickTaskRows(stateContent) {
978
979
  const content = (0, markdown_sectionizer_cjs_1.replaceSection)(stateContent, section, newBody);
979
980
  return { ok: true, value: { content, cleared, variant: match.label } };
980
981
  }
982
+ /**
983
+ * `updateTableCell` scoped to REQUIREMENTS.md's `## Traceability` (or
984
+ * `## Traceability Status`) section (#2245 F1, #5762): `updateTableCell` binds
985
+ * to the first table it finds, and the shipped template puts an `## Out of
986
+ * Scope` table (`Feature | Reason`, no `Status` column) before Traceability, so
987
+ * an unscoped call never reaches the real rows. The ONE copy of this slice
988
+ * (it used to live in both milestone.cts and phase.cts); the heading rule is
989
+ * the Progress owner's — first level-2 heading outside fenced code, running to
990
+ * the next level-1/2 heading. No such heading: the whole text, as before.
991
+ */
992
+ function updateTraceabilityCell(text, match, column, newValue) {
993
+ const headings = (0, markdown_sectionizer_cjs_1.tokenizeHeadings)(text);
994
+ const index = headings.findIndex((h) => h.level === 2 && /^Traceability(?:[ \t]+Status)?\b/i.test(h.text));
995
+ if (index === -1)
996
+ return updateTableCell(text, match, column, newValue);
997
+ let result = { ok: false, reason: 'no matching row' };
998
+ const edited = (0, markdown_sectionizer_cjs_1.editHeadingSlice)(text, (0, markdown_sectionizer_cjs_1.headingSliceRange)(text, headings, index, 2), (scoped) => {
999
+ result = updateTableCell(scoped, match, column, newValue);
1000
+ return result.ok ? result.value : scoped;
1001
+ });
1002
+ return result.ok ? { ok: true, value: edited } : result;
1003
+ }
981
1004
  // Consumers: require('../gsd-core/bin/lib/markdown-table.cjs')
982
1005
  // Named CJS exports are the canonical surface (ADR-457 .cts → .cjs build-at-publish).
@@ -109,10 +109,10 @@ const TOOLS = [
109
109
  type: 'object',
110
110
  properties: {
111
111
  family: { type: 'string', description: 'Command family (e.g. "query", "state", "phase").' },
112
- subcommand: { type: 'string', description: 'Subcommand name.' },
112
+ subcommand: { type: 'string', description: 'Subcommand name, if the command takes one. Omit or leave empty for family-only commands.' },
113
113
  args: { type: 'array', items: {}, description: 'Positional args.' },
114
114
  },
115
- required: ['family', 'subcommand'],
115
+ required: ['family'],
116
116
  },
117
117
  },
118
118
  {
@@ -182,10 +182,13 @@ function callTool(name, args, ctx) {
182
182
  if (name === 'gsd_invoke_command') {
183
183
  const family = asString(a.family);
184
184
  const subcommand = asString(a.subcommand);
185
- if (!family || !subcommand) {
186
- return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires string "family" and "subcommand".' }] };
185
+ if (!family) {
186
+ return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires a non-empty string "family".' }] };
187
187
  }
188
- const res = dispatchGsdCommand({ family, subcommand, args: Array.isArray(a.args) ? a.args : [], cwd });
188
+ if (a.subcommand !== undefined && subcommand === null) {
189
+ return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires string "subcommand" when provided.' }] };
190
+ }
191
+ const res = dispatchGsdCommand({ family, subcommand: subcommand ?? undefined, args: Array.isArray(a.args) ? a.args : [], cwd });
189
192
  if (!res.ok) {
190
193
  return { isError: true, content: [{ type: 'text', text: res.stderr || res.stdout || `dispatch failed (exit ${res.code})` }] };
191
194
  }
@@ -57,6 +57,7 @@ exports.releaseMilestonePhase = releaseMilestonePhase;
57
57
  const node_fs_1 = __importDefault(require("node:fs"));
58
58
  const node_path_1 = __importDefault(require("node:path"));
59
59
  const clock_cjs_1 = require("./clock.cjs");
60
+ const git_info_exclude_cjs_1 = require("./git-info-exclude.cjs");
60
61
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
61
62
  const planningWorkspace = require("./planning-workspace.cjs");
62
63
  // eslint-disable-next-line @typescript-eslint/no-require-imports -- active-workstream-store.cjs is an export= CommonJS module
@@ -122,7 +123,13 @@ function readClaim(cwd) {
122
123
  }
123
124
  function writeClaim(cwd, claim) {
124
125
  ensureClaimDir(cwd);
125
- node_fs_1.default.writeFileSync(milestoneLockPath(cwd), JSON.stringify(claim, null, 2) + '\n');
126
+ const lockPath = milestoneLockPath(cwd);
127
+ node_fs_1.default.writeFileSync(lockPath, JSON.stringify(claim, null, 2) + '\n');
128
+ // #5551: the claim is per-machine state, never project content. Keep it out
129
+ // of `git status` (the ship preflight refuses a dirty tree). Every write, not
130
+ // only the first, so a claim file that predates this fix is covered too; the
131
+ // helper is idempotent and best-effort.
132
+ (0, git_info_exclude_cjs_1.excludeFromGit)(node_path_1.default.dirname(lockPath), exports.MILESTONE_LOCK_FILENAME);
126
133
  }
127
134
  // planningWorkspace.planningDir targets .planning (or the workstream-scoped
128
135
  // variant); the parent is created by every GSD command that gets this far, but
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ /**
3
+ * Milestone Release Map — validation, rendering and archive read-back for the
4
+ * per-repository release record `milestone complete --release-map` persists
5
+ * (#5706, phase 2 of epic #3628, docs/adr/3628-milestone-release-mapping.md).
6
+ *
7
+ * A release map is the caller-supplied record of what each repository shipped
8
+ * for a milestone. It is data: this module never resolves a repository to a
9
+ * directory, never consults `planning.sub_repos`, and never creates a tag. The
10
+ * map is archived by recorded value, so a repository that later leaves
11
+ * `planning.sub_repos` does not change an already-archived map.
12
+ */
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.RELEASE_MAP_TAG_STATES = exports.RELEASE_MAP_STATUSES = exports.MAX_RELEASE_MAP_FILE_BYTES = exports.MAX_RELEASE_MAP_FIELD_LENGTH = exports.MAX_RELEASE_MAP_ENTRIES = void 0;
18
+ exports.validateReleaseMap = validateReleaseMap;
19
+ exports.renderReleaseMapMarkdown = renderReleaseMapMarkdown;
20
+ exports.releaseMapArchivePath = releaseMapArchivePath;
21
+ exports.serializeReleaseMapArchive = serializeReleaseMapArchive;
22
+ exports.loadReleaseMapFile = loadReleaseMapFile;
23
+ exports.readArchivedReleaseMap = readArchivedReleaseMap;
24
+ const node_fs_1 = __importDefault(require("node:fs"));
25
+ const node_path_1 = __importDefault(require("node:path"));
26
+ const security_cjs_1 = require("./security.cjs");
27
+ /** Maximum number of entries a single release map may carry. */
28
+ exports.MAX_RELEASE_MAP_ENTRIES = 256;
29
+ /** Maximum length (UTF-16 code units) of any single string field in an entry. */
30
+ exports.MAX_RELEASE_MAP_FIELD_LENGTH = 200;
31
+ /** Maximum size in bytes of a `--release-map` input file. */
32
+ exports.MAX_RELEASE_MAP_FILE_BYTES = 1048576;
33
+ /** Allowed values of an entry's `status`. */
34
+ exports.RELEASE_MAP_STATUSES = Object.freeze(['released', 'deployed', 'unreleased']);
35
+ /** Allowed values of an entry's `tag`. */
36
+ exports.RELEASE_MAP_TAG_STATES = Object.freeze(['created', 'already_tagged', 'skipped', 'none']);
37
+ const OPTIONAL_STRING_FIELDS = ['release', 'reference', 'environment'];
38
+ const ENTRY_FIELDS = Object.freeze(['repo', ...OPTIONAL_STRING_FIELDS, 'status', 'tag']);
39
+ function isPlainObject(value) {
40
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
41
+ }
42
+ function describeType(value) {
43
+ if (value === null)
44
+ return 'null';
45
+ if (Array.isArray(value))
46
+ return 'array';
47
+ return typeof value;
48
+ }
49
+ /**
50
+ * Validate a release map (an array of entries) and return the normalized
51
+ * entries. Every problem is reported in one pass so a caller fixes the file
52
+ * once. Strict on shape: unknown fields are an error, not silently dropped.
53
+ */
54
+ function validateReleaseMap(input) {
55
+ if (!Array.isArray(input)) {
56
+ return { ok: false, errors: [`release map must be an array of entries (got ${describeType(input)})`] };
57
+ }
58
+ const errors = [];
59
+ if (input.length > exports.MAX_RELEASE_MAP_ENTRIES) {
60
+ errors.push(`release map has ${input.length} entries; the maximum is ${exports.MAX_RELEASE_MAP_ENTRIES}`);
61
+ }
62
+ const map = [];
63
+ const seenRepos = new Set();
64
+ const limit = Math.min(input.length, exports.MAX_RELEASE_MAP_ENTRIES);
65
+ for (let i = 0; i < limit; i++) {
66
+ const raw = input[i];
67
+ const label = `entries[${i}]`;
68
+ if (!isPlainObject(raw)) {
69
+ errors.push(`${label} must be an object (got ${describeType(raw)})`);
70
+ continue;
71
+ }
72
+ let entryOk = true;
73
+ for (const key of Object.keys(raw)) {
74
+ if (!ENTRY_FIELDS.includes(key)) {
75
+ errors.push(`${label} has unknown field "${key.slice(0, exports.MAX_RELEASE_MAP_FIELD_LENGTH)}"`);
76
+ entryOk = false;
77
+ }
78
+ }
79
+ const repo = raw['repo'];
80
+ if (typeof repo !== 'string' || repo.trim() === '') {
81
+ errors.push(`${label}.repo must be a non-blank string`);
82
+ entryOk = false;
83
+ }
84
+ else if (repo.length > exports.MAX_RELEASE_MAP_FIELD_LENGTH) {
85
+ errors.push(`${label}.repo exceeds ${exports.MAX_RELEASE_MAP_FIELD_LENGTH} characters`);
86
+ entryOk = false;
87
+ }
88
+ else if (seenRepos.has(repo)) {
89
+ errors.push(`${label}.repo is a duplicate of an earlier entry`);
90
+ entryOk = false;
91
+ }
92
+ else {
93
+ seenRepos.add(repo);
94
+ }
95
+ const optional = {};
96
+ for (const field of OPTIONAL_STRING_FIELDS) {
97
+ const value = raw[field];
98
+ if (value === undefined || value === null) {
99
+ optional[field] = null;
100
+ }
101
+ else if (typeof value !== 'string') {
102
+ errors.push(`${label}.${field} must be a string or null (got ${describeType(value)})`);
103
+ entryOk = false;
104
+ }
105
+ else if (value.length > exports.MAX_RELEASE_MAP_FIELD_LENGTH) {
106
+ errors.push(`${label}.${field} exceeds ${exports.MAX_RELEASE_MAP_FIELD_LENGTH} characters`);
107
+ entryOk = false;
108
+ }
109
+ else {
110
+ optional[field] = value;
111
+ }
112
+ }
113
+ const status = raw['status'];
114
+ if (typeof status !== 'string' || !exports.RELEASE_MAP_STATUSES.includes(status)) {
115
+ errors.push(`${label}.status must be one of: ${exports.RELEASE_MAP_STATUSES.join(', ')}`);
116
+ entryOk = false;
117
+ }
118
+ const tag = raw['tag'];
119
+ if (typeof tag !== 'string' || !exports.RELEASE_MAP_TAG_STATES.includes(tag)) {
120
+ errors.push(`${label}.tag must be one of: ${exports.RELEASE_MAP_TAG_STATES.join(', ')}`);
121
+ entryOk = false;
122
+ }
123
+ if (entryOk) {
124
+ map.push({
125
+ repo: repo,
126
+ release: optional['release'] ?? null,
127
+ reference: optional['reference'] ?? null,
128
+ environment: optional['environment'] ?? null,
129
+ status: status,
130
+ tag: tag,
131
+ });
132
+ }
133
+ }
134
+ if (errors.length > 0)
135
+ return { ok: false, errors };
136
+ return { ok: true, map };
137
+ }
138
+ /**
139
+ * Escape one value for a GFM table cell: a pipe, newline, backtick, angle
140
+ * bracket or other markdown control character in recorded data can neither
141
+ * end the cell, start a new row, nor inject structure (headings, HTML).
142
+ */
143
+ function escapeCell(value) {
144
+ if (value === null)
145
+ return '-';
146
+ const flattened = value.replace(/[\u0000-\u001f\u007f\u2028\u2029]+/g, ' ').trim();
147
+ if (flattened === '')
148
+ return '-';
149
+ const escaped = flattened.replace(/[\\|`*_[\]<>&~#!]/g, (ch) => {
150
+ if (ch === '<')
151
+ return '&lt;';
152
+ if (ch === '>')
153
+ return '&gt;';
154
+ if (ch === '&')
155
+ return '&amp;';
156
+ return `\\${ch}`;
157
+ });
158
+ return escaped;
159
+ }
160
+ /**
161
+ * Render a validated release map as the `**Release mapping:**` markdown block
162
+ * appended to a MILESTONES.md entry. One table row per entry, always six cells.
163
+ */
164
+ function renderReleaseMapMarkdown(map) {
165
+ const lines = [
166
+ '**Release mapping:**',
167
+ '',
168
+ '| Repository | Release | Reference | Environment | Status | Tag |',
169
+ '| --- | --- | --- | --- | --- | --- |',
170
+ ];
171
+ for (const entry of map) {
172
+ lines.push(`| ${escapeCell(entry.repo)} | ${escapeCell(entry.release)} | ${escapeCell(entry.reference)} | ${escapeCell(entry.environment)} | ${escapeCell(entry.status)} | ${escapeCell(entry.tag)} |`);
173
+ }
174
+ return `${lines.join('\n')}\n`;
175
+ }
176
+ /** Path of the archived release map for a milestone version. */
177
+ function releaseMapArchivePath(milestonesDir, version) {
178
+ return node_path_1.default.join(milestonesDir, `${version}-RELEASE-MAP.json`);
179
+ }
180
+ /** Serialize the archive document `{ version, entries }`. */
181
+ function serializeReleaseMapArchive(version, map) {
182
+ return `${JSON.stringify({ version, entries: map }, null, 2)}\n`;
183
+ }
184
+ /**
185
+ * Read and validate a caller-supplied `--release-map` file. Refuses anything
186
+ * that is not a regular file (directory, FIFO, device), anything over 1 MiB and
187
+ * anything unreadable or unparseable, without ever opening a non-regular file.
188
+ */
189
+ function loadReleaseMapFile(filePath) {
190
+ let stat;
191
+ try {
192
+ stat = node_fs_1.default.statSync(filePath);
193
+ }
194
+ catch (err) {
195
+ const code = err.code ?? 'unknown';
196
+ return { ok: false, errors: [`release map file cannot be read (${code}): ${node_path_1.default.basename(filePath)}`] };
197
+ }
198
+ if (!stat.isFile()) {
199
+ return { ok: false, errors: [`release map path is not a regular file: ${node_path_1.default.basename(filePath)}`] };
200
+ }
201
+ if (stat.size > exports.MAX_RELEASE_MAP_FILE_BYTES) {
202
+ return { ok: false, errors: [`release map file is ${stat.size} bytes; the maximum is ${exports.MAX_RELEASE_MAP_FILE_BYTES}`] };
203
+ }
204
+ let text;
205
+ try {
206
+ text = node_fs_1.default.readFileSync(filePath, 'utf-8');
207
+ }
208
+ catch (err) {
209
+ const code = err.code ?? 'unknown';
210
+ return { ok: false, errors: [`release map file cannot be read (${code}): ${node_path_1.default.basename(filePath)}`] };
211
+ }
212
+ const parsed = (0, security_cjs_1.safeJsonParse)(text, { maxLength: exports.MAX_RELEASE_MAP_FILE_BYTES, label: 'release map' });
213
+ if (!parsed.ok) {
214
+ return { ok: false, errors: [parsed.error ?? 'release map: invalid JSON'] };
215
+ }
216
+ return validateReleaseMap(parsed.value);
217
+ }
218
+ /**
219
+ * Read a milestone's archived release map back by recorded value. A milestone
220
+ * archived without a map yields `{ ok: true, map: null }` ("no map"), never an
221
+ * error; a present-but-corrupt archive is reported, not guessed at.
222
+ */
223
+ function readArchivedReleaseMap(milestonesDir, version) {
224
+ const file = releaseMapArchivePath(milestonesDir, version);
225
+ let text;
226
+ try {
227
+ text = node_fs_1.default.readFileSync(file, 'utf-8');
228
+ }
229
+ catch (err) {
230
+ if (err.code === 'ENOENT')
231
+ return { ok: true, map: null };
232
+ return { ok: false, error: `release map archive cannot be read: ${node_path_1.default.basename(file)}` };
233
+ }
234
+ const parsed = (0, security_cjs_1.safeJsonParse)(text, { maxLength: exports.MAX_RELEASE_MAP_FILE_BYTES, label: 'release map archive' });
235
+ if (!parsed.ok)
236
+ return { ok: false, error: parsed.error ?? 'release map archive: invalid JSON' };
237
+ const doc = parsed.value;
238
+ if (!isPlainObject(doc))
239
+ return { ok: false, error: 'release map archive: expected an object' };
240
+ const validated = validateReleaseMap(doc['entries']);
241
+ if (!validated.ok)
242
+ return { ok: false, error: validated.errors.join('; ') };
243
+ return { ok: true, map: validated.map };
244
+ }