@opengsd/gsd-core 1.18.0 → 1.19.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 +2 -2
- package/agents/gsd-code-fixer.md +4 -3
- package/agents/gsd-verifier.md +1 -1
- package/bin/install.js +422 -103
- package/gsd-core/bin/gsd-tools.cjs +106 -7
- 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-lock.cjs +13 -2
- package/gsd-core/bin/lib/capability-registry.cjs +156 -74
- package/gsd-core/bin/lib/capability-state.cjs +14 -8
- package/gsd-core/bin/lib/capability-validator.cjs +17 -0
- package/gsd-core/bin/lib/config-loader.cjs +10 -0
- package/gsd-core/bin/lib/config.cjs +256 -58
- package/gsd-core/bin/lib/core-utils.cjs +34 -0
- package/gsd-core/bin/lib/decision-coverage-support.cjs +74 -0
- package/gsd-core/bin/lib/gate-context-drift.cjs +8 -3
- 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-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 +57 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +18 -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 +65 -1
- 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/markdown-line-diff.cjs +147 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +113 -0
- 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 +43 -1
- 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-id.cjs +21 -4
- package/gsd-core/bin/lib/phase-lifecycle.cjs +37 -4
- package/gsd-core/bin/lib/phase.cjs +115 -26
- package/gsd-core/bin/lib/planning-snapshot.cjs +3 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +18 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
- package/gsd-core/bin/lib/read-only-git-env.cjs +58 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +35 -2
- package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +12 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +39 -2
- package/gsd-core/bin/lib/roadmap.cjs +33 -25
- 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 +121 -16
- package/gsd-core/bin/lib/smart-entry.cjs +13 -0
- package/gsd-core/bin/lib/state-contract.cjs +29 -9
- package/gsd-core/bin/lib/state-document.cjs +12 -4
- package/gsd-core/bin/lib/state-transition.cjs +119 -24
- package/gsd-core/bin/lib/state.cjs +50 -28
- 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/verification.cjs +96 -9
- package/gsd-core/bin/lib/verify.cjs +2 -0
- 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/references/git-integration.md +11 -0
- package/gsd-core/references/loop-hook-dispatch.md +13 -6
- 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.md +61 -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 +9 -5
- 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.md +13 -13
- 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/review.md +16 -13
- package/gsd-core/workflows/section-manifest.json +5 -0
- package/gsd-core/workflows/settings.md +1 -0
- package/gsd-core/workflows/ship.md +3 -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-path-export.js +95 -0
- package/hooks/dist/gsd-secret-read-guard.js +172 -39
- package/hooks/dist/gsd-statusline.js +35 -2
- package/hooks/dist/gsd-windsurf-pre-write.js +28 -1
- package/hooks/dist/gsd-workflow-guard.js +1 -1
- package/hooks/dist/gsd-worktree-path-guard.js +28 -1
- 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-path-export.js +95 -0
- package/hooks/gsd-secret-read-guard.js +172 -39
- package/hooks/gsd-statusline.js +35 -2
- package/hooks/gsd-windsurf-pre-write.js +28 -1
- package/hooks/gsd-workflow-guard.js +1 -1
- package/hooks/gsd-worktree-path-guard.js +28 -1
- package/hooks/hooks.json +1 -0
- package/hooks/lib/isolation-sentinel.js +44 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +13 -6
- 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 +769 -0
- package/pi/package-agents/gsd-code-reviewer.md +402 -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 +894 -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 +51 -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/command-contract-helpers.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +20 -7
- package/scripts/gen-health-docs.cjs +13 -1
- package/scripts/gen-pi-package-skills.cjs +217 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +11 -1
- package/scripts/lib/platform-conformance-tier.generated.cjs +26 -1
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
- package/scripts/lint-retired-runtime-name.cjs +5 -0
- 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/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,6 +25,9 @@ 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;
|
|
30
33
|
exports.deleteSection = deleteSection;
|
|
@@ -850,6 +853,116 @@ function stripTaggedBlocks(content, tagName, allowAttributes = false, caseSensit
|
|
|
850
853
|
return content;
|
|
851
854
|
return content.replace(taggedBlockPattern(tagName, caseSensitive ? 'g' : 'gi', allowAttributes), '');
|
|
852
855
|
}
|
|
856
|
+
/**
|
|
857
|
+
* Run `edit` over `content` with every `<tagName>…</tagName>` block hidden from
|
|
858
|
+
* it, then put the blocks back byte-for-byte (#5543). The blocks are swapped for
|
|
859
|
+
* one-line placeholder comments, so `edit` sees the rest of the document in its
|
|
860
|
+
* original order and cannot touch, or be captured by, a block's contents.
|
|
861
|
+
*
|
|
862
|
+
* Same hardened block pattern as `stripTaggedBlocks` (exact `<tag>` opener, so
|
|
863
|
+
* `<details open>` stays visible to `edit`). If `edit` returns a non-string, or
|
|
864
|
+
* drops a placeholder, the original `content` is returned unchanged rather than
|
|
865
|
+
* a document with a block missing.
|
|
866
|
+
*
|
|
867
|
+
* Nested blocks are NOT supported, exactly as for `extractTaggedBlocks` and
|
|
868
|
+
* `stripTaggedBlocks`: the first `</tag>` closes the block. Every ROADMAP reader
|
|
869
|
+
* that strips shipped milestones shares that grammar, so this writer sees the
|
|
870
|
+
* same blocks those readers do.
|
|
871
|
+
*/
|
|
872
|
+
function editOutsideTaggedBlocks(content, tagName, edit, allowAttributes = false, caseSensitive = false) {
|
|
873
|
+
if (typeof content !== 'string' || content.length === 0)
|
|
874
|
+
return content;
|
|
875
|
+
if (typeof tagName !== 'string' || tagName.length === 0)
|
|
876
|
+
return edit(content);
|
|
877
|
+
const blocks = [];
|
|
878
|
+
let nonce = 0;
|
|
879
|
+
while (content.includes(`<!-- gsd-masked-${tagName}-${nonce}-`))
|
|
880
|
+
nonce += 1;
|
|
881
|
+
const placeholder = (i) => `<!-- gsd-masked-${tagName}-${nonce}-${i} -->`;
|
|
882
|
+
const masked = content.replace(taggedBlockPattern(tagName, caseSensitive ? 'g' : 'gi', allowAttributes), (block) => {
|
|
883
|
+
blocks.push(block);
|
|
884
|
+
return placeholder(blocks.length - 1);
|
|
885
|
+
});
|
|
886
|
+
if (blocks.length === 0)
|
|
887
|
+
return edit(content);
|
|
888
|
+
const edited = edit(masked);
|
|
889
|
+
if (typeof edited !== 'string')
|
|
890
|
+
return content;
|
|
891
|
+
let restored = edited;
|
|
892
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
893
|
+
const marker = placeholder(i);
|
|
894
|
+
const at = restored.indexOf(marker);
|
|
895
|
+
if (at === -1 || restored.indexOf(marker, at + marker.length) !== -1)
|
|
896
|
+
return content;
|
|
897
|
+
restored = restored.slice(0, at) + blocks[i] + restored.slice(at + marker.length);
|
|
898
|
+
}
|
|
899
|
+
return restored;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Hide every fenced code block from a line-oriented edit. State-transition writers locate their
|
|
903
|
+
* target with `/^Field:.*$/m`-style patterns; those match a fenced EXAMPLE of the field as readily
|
|
904
|
+
* as the live line, so the example got the new value and the live line stayed stale. Masking each
|
|
905
|
+
* block to a single `<!-- gsd-masked-fence-… -->` line lets the same pattern see only live lines;
|
|
906
|
+
* `restore` splices the blocks back byte-for-byte (CRLF included — the original line text is kept).
|
|
907
|
+
* The fence grammar is `scanFencedBlocks`' (the one every reader here uses), and an unterminated
|
|
908
|
+
* fence masks to the end of the content, as `stripFencedCode` treats it.
|
|
909
|
+
*/
|
|
910
|
+
function maskFences(content) {
|
|
911
|
+
const identity = { masked: content, restore: (edited) => edited };
|
|
912
|
+
if (typeof content !== 'string' || content.length === 0)
|
|
913
|
+
return identity;
|
|
914
|
+
const lines = content.split('\n');
|
|
915
|
+
const blocks = scanFencedBlocks(lines);
|
|
916
|
+
if (blocks.length === 0)
|
|
917
|
+
return identity;
|
|
918
|
+
let nonce = 0;
|
|
919
|
+
while (content.includes(`<!-- gsd-masked-fence-${nonce}-`))
|
|
920
|
+
nonce += 1;
|
|
921
|
+
const placeholder = (i) => `<!-- gsd-masked-fence-${nonce}-${i} -->`;
|
|
922
|
+
const kept = [];
|
|
923
|
+
const originals = [];
|
|
924
|
+
let cursor = 0;
|
|
925
|
+
for (const block of blocks) {
|
|
926
|
+
const end = block.closeLineIdx === -1 ? lines.length - 1 : block.closeLineIdx;
|
|
927
|
+
for (; cursor < block.openLineIdx; cursor++)
|
|
928
|
+
kept.push(lines[cursor]);
|
|
929
|
+
originals.push(lines.slice(block.openLineIdx, end + 1).join('\n'));
|
|
930
|
+
kept.push(placeholder(originals.length - 1));
|
|
931
|
+
cursor = end + 1;
|
|
932
|
+
}
|
|
933
|
+
for (; cursor < lines.length; cursor++)
|
|
934
|
+
kept.push(lines[cursor]);
|
|
935
|
+
return {
|
|
936
|
+
masked: kept.join('\n'),
|
|
937
|
+
restore(edited) {
|
|
938
|
+
if (typeof edited !== 'string')
|
|
939
|
+
return null;
|
|
940
|
+
let restored = edited;
|
|
941
|
+
for (let i = 0; i < originals.length; i++) {
|
|
942
|
+
const marker = placeholder(i);
|
|
943
|
+
const at = restored.indexOf(marker);
|
|
944
|
+
if (at === -1 || restored.indexOf(marker, at + marker.length) !== -1)
|
|
945
|
+
return null;
|
|
946
|
+
restored = restored.slice(0, at) + originals[i] + restored.slice(at + marker.length);
|
|
947
|
+
}
|
|
948
|
+
return restored;
|
|
949
|
+
},
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Run `edit` over `content` with every fenced code block hidden from it (see `maskFences`).
|
|
954
|
+
* Content without a fence is edited as given. An edit that returns a non-string, or that loses a
|
|
955
|
+
* placeholder, returns the original content unchanged.
|
|
956
|
+
*/
|
|
957
|
+
function editOutsideFences(content, edit) {
|
|
958
|
+
if (typeof content !== 'string' || content.length === 0)
|
|
959
|
+
return content;
|
|
960
|
+
const mask = maskFences(content);
|
|
961
|
+
const edited = edit(mask.masked);
|
|
962
|
+
if (typeof edited !== 'string')
|
|
963
|
+
return content;
|
|
964
|
+
return mask.restore(edited) ?? content;
|
|
965
|
+
}
|
|
853
966
|
// ─── replaceSection ───────────────────────────────────────────────────────────
|
|
854
967
|
/**
|
|
855
968
|
* Splice `newBody` in place of a section's body and return the resulting
|
|
@@ -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
|
+
}
|