@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -1
- package/.opencode/plugins/gsd-core.js +2 -3
- package/agents/gsd-code-fixer.compact.md +7 -7
- package/agents/gsd-code-fixer.md +13 -7
- package/agents/gsd-code-reviewer.compact.md +49 -8
- package/agents/gsd-code-reviewer.md +53 -14
- package/agents/gsd-executor.md +8 -4
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +422 -103
- package/commands/gsd/update.md +5 -2
- package/gsd-core/bin/gsd-tools.cjs +179 -30
- package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
- package/gsd-core/bin/lib/args-terminator.cjs +114 -0
- package/gsd-core/bin/lib/broken-windows.cjs +59 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
- package/gsd-core/bin/lib/capability-lock.cjs +13 -2
- package/gsd-core/bin/lib/capability-registry.cjs +376 -103
- package/gsd-core/bin/lib/capability-state.cjs +14 -8
- package/gsd-core/bin/lib/capability-validator.cjs +32 -0
- package/gsd-core/bin/lib/commands.cjs +27 -17
- package/gsd-core/bin/lib/config-loader.cjs +13 -3
- package/gsd-core/bin/lib/config.cjs +256 -58
- package/gsd-core/bin/lib/core-utils.cjs +45 -0
- package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +21 -3
- package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
- package/gsd-core/bin/lib/gate-context-drift.cjs +9 -4
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +6 -1
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +6 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
- package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
- package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +63 -22
- package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
- package/gsd-core/bin/lib/git-info-exclude.cjs +52 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +31 -12
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
- package/gsd-core/bin/lib/host-integration.cjs +13 -6
- package/gsd-core/bin/lib/init.cjs +245 -68
- package/gsd-core/bin/lib/install-engine.cjs +246 -32
- package/gsd-core/bin/lib/install-profiles.cjs +9 -4
- package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +147 -1
- package/gsd-core/bin/lib/markdown-table.cjs +23 -0
- package/gsd-core/bin/lib/mcp-server.cjs +8 -5
- package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
- package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
- package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
- package/gsd-core/bin/lib/milestone.cjs +61 -51
- package/gsd-core/bin/lib/model-catalog.cjs +8 -0
- package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
- package/gsd-core/bin/lib/phase-id.cjs +629 -38
- package/gsd-core/bin/lib/phase-lifecycle.cjs +12 -44
- package/gsd-core/bin/lib/phase-locator.cjs +328 -19
- package/gsd-core/bin/lib/phase.cjs +3066 -653
- package/gsd-core/bin/lib/planning-document.cjs +26 -5
- package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
- package/gsd-core/bin/lib/planning-snapshot.cjs +15 -8
- package/gsd-core/bin/lib/planning-workspace.cjs +21 -3
- package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
- package/gsd-core/bin/lib/read-only-git-env.cjs +79 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +57 -2
- package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
- package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +735 -137
- package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -7
- package/gsd-core/bin/lib/roadmap.cjs +383 -123
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +381 -14
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
- package/gsd-core/bin/lib/shell-command-projection.cjs +151 -19
- package/gsd-core/bin/lib/smart-entry.cjs +35 -11
- package/gsd-core/bin/lib/state-contract.cjs +39 -15
- package/gsd-core/bin/lib/state-document.cjs +344 -27
- package/gsd-core/bin/lib/state-transition.cjs +156 -32
- package/gsd-core/bin/lib/state.cjs +527 -186
- package/gsd-core/bin/lib/surface.cjs +9 -0
- package/gsd-core/bin/lib/task-command-router.cjs +5 -25
- package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
- package/gsd-core/bin/lib/template.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +3 -2
- package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
- package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
- package/gsd-core/bin/lib/verification.cjs +96 -9
- package/gsd-core/bin/lib/verify.cjs +4 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
- package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
- package/gsd-core/references/cicd-review-patterns.md +45 -0
- package/gsd-core/references/git-integration.md +11 -0
- package/gsd-core/references/loop-hook-dispatch.md +13 -6
- package/gsd-core/references/necessity-review.md +48 -0
- package/gsd-core/references/planning-config.md +2 -0
- package/gsd-core/references/tdd.md +2 -2
- package/gsd-core/templates/DEBUG.md +16 -0
- package/gsd-core/templates/config.json +2 -1
- package/gsd-core/templates/verification-report.md +1 -1
- package/gsd-core/workflows/audit-fix.md +2 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
- package/gsd-core/workflows/code-review-fix.md +46 -5
- package/gsd-core/workflows/code-review.md +64 -7
- package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
- package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
- package/gsd-core/workflows/complete-milestone.md +37 -150
- package/gsd-core/workflows/diagnose-issues.md +29 -5
- package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +33 -2
- package/gsd-core/workflows/discuss-phase-power.md +19 -2
- package/gsd-core/workflows/discuss-phase.md +6 -10
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -1
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
- package/gsd-core/workflows/execute-phase.md +10 -6
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/health.md +2 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +2 -2
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
- package/gsd-core/workflows/plan-phase.md +14 -14
- package/gsd-core/workflows/pr-branch.md +2 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
- package/gsd-core/workflows/quick.md +1 -1
- package/gsd-core/workflows/reapply-patches.md +104 -0
- package/gsd-core/workflows/review.md +70 -154
- package/gsd-core/workflows/section-manifest.json +5 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +57 -9
- package/gsd-core/workflows/settings.md +1 -0
- package/gsd-core/workflows/ship.md +15 -4
- package/gsd-core/workflows/spec-phase.md +3 -6
- package/gsd-core/workflows/verify-work.md +4 -3
- package/hooks/dist/gsd-agent-isolation-guard.js +11 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +6 -0
- package/hooks/dist/gsd-path-export.js +95 -0
- package/hooks/dist/gsd-secret-read-guard.js +172 -39
- package/hooks/dist/gsd-statusline.js +37 -3
- package/hooks/dist/gsd-windsurf-pre-write.js +29 -2
- package/hooks/dist/gsd-workflow-guard.js +3 -2
- package/hooks/dist/gsd-worktree-path-guard.js +29 -2
- package/hooks/dist/lib/git-probe.js +16 -0
- package/hooks/dist/lib/isolation-sentinel.js +44 -0
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-agent-isolation-guard.js +11 -1
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +6 -0
- package/hooks/gsd-path-export.js +95 -0
- package/hooks/gsd-secret-read-guard.js +172 -39
- package/hooks/gsd-statusline.js +37 -3
- package/hooks/gsd-windsurf-pre-write.js +29 -2
- package/hooks/gsd-workflow-guard.js +3 -2
- package/hooks/gsd-worktree-path-guard.js +29 -2
- package/hooks/hooks.json +1 -0
- package/hooks/lib/git-probe.js +16 -0
- package/hooks/lib/isolation-sentinel.js +44 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +15 -8
- package/pi/gsd.cjs +1047 -10
- package/pi/package-agents/gsd-advisor-researcher.md +113 -0
- package/pi/package-agents/gsd-ai-researcher.md +111 -0
- package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
- package/pi/package-agents/gsd-code-fixer.md +774 -0
- package/pi/package-agents/gsd-code-reviewer.md +441 -0
- package/pi/package-agents/gsd-codebase-mapper.md +850 -0
- package/pi/package-agents/gsd-debug-session-manager.md +416 -0
- package/pi/package-agents/gsd-debugger.md +1274 -0
- package/pi/package-agents/gsd-doc-classifier.md +269 -0
- package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
- package/pi/package-agents/gsd-doc-verifier.md +212 -0
- package/pi/package-agents/gsd-doc-writer.md +626 -0
- package/pi/package-agents/gsd-dom-verifier.md +164 -0
- package/pi/package-agents/gsd-domain-researcher.md +144 -0
- package/pi/package-agents/gsd-eval-auditor.md +185 -0
- package/pi/package-agents/gsd-eval-planner.md +149 -0
- package/pi/package-agents/gsd-executor.md +898 -0
- package/pi/package-agents/gsd-framework-selector.md +159 -0
- package/pi/package-agents/gsd-integration-checker.md +476 -0
- package/pi/package-agents/gsd-intel-updater.md +338 -0
- package/pi/package-agents/gsd-mempalace-curator.md +49 -0
- package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
- package/pi/package-agents/gsd-pattern-mapper.md +340 -0
- package/pi/package-agents/gsd-phase-researcher.md +897 -0
- package/pi/package-agents/gsd-plan-checker.md +1077 -0
- package/pi/package-agents/gsd-planner.md +1003 -0
- package/pi/package-agents/gsd-project-researcher.md +611 -0
- package/pi/package-agents/gsd-research-synthesizer.md +259 -0
- package/pi/package-agents/gsd-roadmapper.md +779 -0
- package/pi/package-agents/gsd-security-auditor.md +171 -0
- package/pi/package-agents/gsd-ui-auditor.md +581 -0
- package/pi/package-agents/gsd-ui-checker.md +417 -0
- package/pi/package-agents/gsd-ui-researcher.md +469 -0
- package/pi/package-agents/gsd-user-profiler.md +174 -0
- package/pi/package-agents/gsd-verifier.md +963 -0
- package/pi/package-agents/manifest.json +39 -0
- package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
- package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
- package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
- package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
- package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
- package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
- package/pi/package-skills/gsd-capture/SKILL.md +68 -0
- package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
- package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
- package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
- package/pi/package-skills/gsd-config/SKILL.md +60 -0
- package/pi/package-skills/gsd-debug/SKILL.md +64 -0
- package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
- package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
- package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-explore/SKILL.md +26 -0
- package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
- package/pi/package-skills/gsd-fast/SKILL.md +33 -0
- package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
- package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
- package/pi/package-skills/gsd-health/SKILL.md +36 -0
- package/pi/package-skills/gsd-help/SKILL.md +37 -0
- package/pi/package-skills/gsd-import/SKILL.md +46 -0
- package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
- package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
- package/pi/package-skills/gsd-manager/SKILL.md +46 -0
- package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
- package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
- package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
- package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
- package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
- package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
- package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
- package/pi/package-skills/gsd-next/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
- package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
- package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
- package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
- package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
- package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
- package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
- package/pi/package-skills/gsd-phase/SKILL.md +61 -0
- package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
- package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
- package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
- package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
- package/pi/package-skills/gsd-progress/SKILL.md +51 -0
- package/pi/package-skills/gsd-quick/SKILL.md +179 -0
- package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
- package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
- package/pi/package-skills/gsd-review/SKILL.md +50 -0
- package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
- package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-settings/SKILL.md +30 -0
- package/pi/package-skills/gsd-ship/SKILL.md +27 -0
- package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
- package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
- package/pi/package-skills/gsd-spike/SKILL.md +53 -0
- package/pi/package-skills/gsd-stats/SKILL.md +22 -0
- package/pi/package-skills/gsd-surface/SKILL.md +177 -0
- package/pi/package-skills/gsd-thread/SKILL.md +29 -0
- package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
- package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
- package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
- package/pi/package-skills/gsd-undo/SKILL.md +38 -0
- package/pi/package-skills/gsd-update/SKILL.md +54 -0
- package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
- package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
- package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
- package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
- package/scripts/build-hooks.js +4 -0
- package/scripts/ci-next-health.cjs +246 -23
- package/scripts/command-contract-helpers.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +22 -7
- package/scripts/gen-features.cjs +14 -2
- package/scripts/gen-health-docs.cjs +13 -1
- package/scripts/gen-pi-package-skills.cjs +217 -0
- package/scripts/lib/drift-scan.cjs +27 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +17 -1
- package/scripts/lib/platform-conformance-tier.generated.cjs +37 -1
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
- package/scripts/lint-phase-enumeration-drift.cjs +8 -4
- package/scripts/lint-retired-runtime-name.cjs +5 -0
- package/scripts/lint-roadmap-progress-drift.cjs +421 -0
- package/scripts/mutation-matrix.cjs +23 -1
- package/scripts/npm-audit-baseline.cjs +41 -7
- package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
- package/scripts/select-docs-guards.cjs +1 -1
- package/scripts/self-test-report-policy.cjs +350 -0
- package/scripts/sync-next-version.cjs +38 -6
- package/skills/gsd-update/SKILL.md +5 -2
- package/vscode/package.json +1 -1
- package/scripts/diagnose-validate-commit-in-tier.cjs +0 -270
- package/scripts/diagnose-validate-commit-phases.cjs +0 -348
- 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
|
|
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'
|
|
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
|
|
186
|
-
return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires string "family"
|
|
185
|
+
if (!family) {
|
|
186
|
+
return { isError: true, content: [{ type: 'text', text: 'gsd_invoke_command requires a non-empty string "family".' }] };
|
|
187
187
|
}
|
|
188
|
-
|
|
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
|
-
|
|
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 '<';
|
|
152
|
+
if (ch === '>')
|
|
153
|
+
return '>';
|
|
154
|
+
if (ch === '&')
|
|
155
|
+
return '&';
|
|
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
|
+
}
|