@opengsd/gsd-core 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-debug-session-manager.md +11 -0
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +5 -5
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +37 -0
- package/agents/gsd-planner.md +44 -46
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +12 -3
- package/bin/install.js +841 -971
- package/bin/lib/ui-safety-gate.cjs +2 -0
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/quick.md +1 -1
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +469 -88
- package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
- package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
- package/gsd-core/bin/lib/api-coverage.cjs +3 -5
- package/gsd-core/bin/lib/artifacts.cjs +3 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +876 -240
- package/gsd-core/bin/lib/broken-windows.cjs +1 -1
- package/gsd-core/bin/lib/capability-consent.cjs +149 -15
- package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
- package/gsd-core/bin/lib/capability-registry.cjs +575 -101
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +495 -22
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +71 -37
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
- package/gsd-core/bin/lib/command-aliases.cjs +22 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +651 -86
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +75 -0
- package/gsd-core/bin/lib/config.cjs +10 -1
- package/gsd-core/bin/lib/core-utils.cjs +127 -29
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +155 -20
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init.cjs +321 -129
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +745 -258
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
- package/gsd-core/bin/lib/install-profiles.cjs +134 -57
- package/gsd-core/bin/lib/install-scope.cjs +270 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
- package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
- package/gsd-core/bin/lib/io.cjs +10 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +754 -70
- package/gsd-core/bin/lib/model-catalog.cjs +59 -1
- package/gsd-core/bin/lib/model-resolver.cjs +183 -40
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
- package/gsd-core/bin/lib/phase-id.cjs +444 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +125 -18
- package/gsd-core/bin/lib/phase.cjs +646 -143
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +86 -2
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
- package/gsd-core/bin/lib/probe-core.cjs +1 -1
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +385 -94
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
- package/gsd-core/bin/lib/smart-entry.cjs +142 -22
- package/gsd-core/bin/lib/state-command-router.cjs +5 -1
- package/gsd-core/bin/lib/state-document.cjs +152 -8
- package/gsd-core/bin/lib/state-transition.cjs +371 -117
- package/gsd-core/bin/lib/state.cjs +1794 -357
- package/gsd-core/bin/lib/surface.cjs +23 -9
- package/gsd-core/bin/lib/text-lines.cjs +80 -0
- package/gsd-core/bin/lib/token-scanner.cjs +76 -0
- package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
- package/gsd-core/bin/lib/uat.cjs +399 -56
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
- package/gsd-core/bin/lib/unusable-input.cjs +24 -0
- package/gsd-core/bin/lib/update-context.cjs +8 -2
- package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +37 -0
- package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
- package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +258 -8
- package/gsd-core/bin/lib/verify.cjs +368 -888
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
- package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
- package/gsd-core/bin/lib/workstream.cjs +2 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/execute-mvp-tdd.md +3 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
- package/gsd-core/references/execute-phase-context-guard.md +1 -1
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +6 -2
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-planning-commit.md +2 -1
- package/gsd-core/references/loop-hook-dispatch.md +39 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planning-config.md +8 -6
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +2 -4
- package/gsd-core/templates/state.md +4 -4
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/autonomous.md +1 -1
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +38 -12
- package/gsd-core/workflows/complete-milestone.md +141 -18
- package/gsd-core/workflows/debug.md +7 -5
- package/gsd-core/workflows/diagnose-issues.md +35 -9
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
- package/gsd-core/workflows/edit-phase.md +26 -1
- package/gsd-core/workflows/eval-review.md +3 -5
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
- package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
- package/gsd-core/workflows/execute-phase.md +38 -50
- package/gsd-core/workflows/execute-plan.md +36 -4
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/health.md +73 -4
- package/gsd-core/workflows/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +5 -5
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone.md +14 -9
- package/gsd-core/workflows/new-project.md +14 -14
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase.md +41 -17
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress.md +34 -6
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
- package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
- package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
- package/gsd-core/workflows/quick.md +35 -15
- package/gsd-core/workflows/review.md +26 -5
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings.md +66 -2
- package/gsd-core/workflows/ship.md +104 -44
- package/gsd-core/workflows/spec-phase.md +30 -12
- package/gsd-core/workflows/sync-skills.md +63 -8
- package/gsd-core/workflows/transition.md +46 -11
- package/gsd-core/workflows/ui-phase.md +5 -5
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update.md +1 -1
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +9 -7
- package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
- package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/dist/gsd-prompt-guard.js +21 -20
- package/hooks/dist/gsd-read-injection-scanner.js +38 -24
- package/hooks/dist/gsd-statusline.js +18 -0
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/injection-patterns.js +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +9 -0
- package/hooks/gsd-agent-isolation-guard.js +103 -14
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +77 -2
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +38 -24
- package/hooks/gsd-statusline.js +18 -0
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +9 -0
- package/package.json +21 -9
- package/pi/gsd.cjs +19 -5
- package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
- package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
- package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
- package/scripts/changeset/lint.cjs +60 -5
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/ci-test-scope.cjs +19 -2
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +2 -11
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/drift-scan.cjs +278 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-phase-enumeration-drift.cjs +479 -0
- package/scripts/lint-plan-count-drift.cjs +318 -0
- package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
- package/scripts/lint-planning-prompt-drift.cjs +434 -0
- package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
- package/scripts/lint-regression-test-names.cjs +15 -13
- package/scripts/lint-removed-but-needed.cjs +320 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +1045 -0
- package/scripts/lint-test-file-count.allowlist.json +21 -10
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +25 -0
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/state-write-path-drift-baseline.json +19 -0
- package/scripts/sync-runtime-launcher.cjs +2 -4
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +0 -1
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +1 -1
- package/skills/gsd-review-backlog/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +0 -1
- package/skills/gsd-verify-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Planning Snapshot — a parsed projection of `.planning/` (Phase 10, #3308,
|
|
4
|
+
* ADR-3180 §8.1).
|
|
5
|
+
*
|
|
6
|
+
* Composed EXCLUSIVELY from the already-consolidated §7 owners
|
|
7
|
+
* (`getMilestoneInfo`, `listMilestonePhaseDirs`, `isPhaseComplete`,
|
|
8
|
+
* `scanPhasePlans`, `stateFieldValue`, `planningPaths`) plus the frozen
|
|
9
|
+
* `SCOPE` enum. This module introduces no new semantic derivation — it
|
|
10
|
+
* introduces exactly one new thing: `worstScope`, a way to combine several
|
|
11
|
+
* independently-scoped owner answers into one composite record without
|
|
12
|
+
* letting a caller treat a non-answer as data.
|
|
13
|
+
*
|
|
14
|
+
* `buildPlanningSnapshot(cwd)` is the sole export consumers reach for;
|
|
15
|
+
* `worstScope` is exported alongside it for direct unit coverage.
|
|
16
|
+
*
|
|
17
|
+
* Design: .gsd/phase/refactor-3308-planning-snapshot-parsed-projection/40-design.md
|
|
18
|
+
*
|
|
19
|
+
* ADR-457 build-at-publish: source in src/planning-snapshot.cts, compiled to
|
|
20
|
+
* gsd-core/bin/lib/planning-snapshot.cjs (gitignored).
|
|
21
|
+
*/
|
|
22
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
23
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
24
|
+
};
|
|
25
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
26
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
|
+
const roadmapParserMod = require("./roadmap-parser.cjs");
|
|
29
|
+
const { getMilestoneInfo, extractCurrentMilestone } = roadmapParserMod;
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
|
+
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
32
|
+
const { listMilestonePhaseDirs } = phaseLocatorMod;
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
34
|
+
const verificationMod = require("./verification.cjs");
|
|
35
|
+
const { isPhaseComplete } = verificationMod;
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
|
+
const scanPhasePlans = require("./plan-scan.cjs");
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
|
+
const planningWorkspace = require("./planning-workspace.cjs");
|
|
40
|
+
const { planningPaths, planningRoot } = planningWorkspace;
|
|
41
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
43
|
+
const frontmatterMod = require("./frontmatter.cjs");
|
|
44
|
+
const { extractFrontmatter, stripFrontmatter } = frontmatterMod;
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- core-utils.cjs is an export= CommonJS module
|
|
46
|
+
const coreUtilsMod = require("./core-utils.cjs");
|
|
47
|
+
const { findOrphanSummaries } = coreUtilsMod;
|
|
48
|
+
const state_document_cjs_1 = require("./state-document.cjs");
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
|
+
const unusableInputMod = require("./unusable-input.cjs");
|
|
51
|
+
const { UNUSABLE_REASON, warnUnusableInput } = unusableInputMod;
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
53
|
+
const planningScopeMod = require("./planning-scope.cjs");
|
|
54
|
+
const { SCOPE } = planningScopeMod;
|
|
55
|
+
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- agent-install-check.cjs is an export= CommonJS module
|
|
57
|
+
const agentInstallCheckMod = require("./agent-install-check.cjs");
|
|
58
|
+
const { checkAgentsInstalled } = agentInstallCheckMod;
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- worktree-safety.cjs is an export= CommonJS module
|
|
60
|
+
const worktreeSafetyMod = require("./worktree-safety.cjs");
|
|
61
|
+
const { inspectWorktreeHealth } = worktreeSafetyMod;
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
|
|
63
|
+
const configLoaderMod = require("./config-loader.cjs");
|
|
64
|
+
const { isGitIgnored } = configLoaderMod;
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
66
|
+
const phaseIdMod = require("./phase-id.cjs");
|
|
67
|
+
const { PHASE_NUMBER_TOKEN_SOURCE, OPTIONAL_PHASE_TAG_SOURCE, stripProjectCodePrefix, scopeToPhase } = phaseIdMod;
|
|
68
|
+
const validate_cjs_1 = require("./validate.cjs");
|
|
69
|
+
// ─── worstScope — the one new piece of coordination logic ───────────────────
|
|
70
|
+
/**
|
|
71
|
+
* Severity ordering (`UNREADABLE` worst, `COMPLETE` best) is a genuine design
|
|
72
|
+
* choice, not inherited from anywhere — see the design doc's "Scope
|
|
73
|
+
* combination" section. `TRUNCATED` vs `UNSCOPED` are not ranked against each
|
|
74
|
+
* other by any upstream decision; this ordering exists only so a future
|
|
75
|
+
* diagnostic rule can name which failure was worse when several compound.
|
|
76
|
+
*/
|
|
77
|
+
const SCOPE_SEVERITY = {
|
|
78
|
+
[SCOPE.COMPLETE]: 0,
|
|
79
|
+
[SCOPE.TRUNCATED]: 1,
|
|
80
|
+
[SCOPE.UNSCOPED]: 2,
|
|
81
|
+
[SCOPE.UNREADABLE]: 3,
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Combine several independently-scoped owner answers into the single worst
|
|
85
|
+
* (most severe) `Scope` among them. Pure, no I/O. Not a re-derivation of any
|
|
86
|
+
* §7 owner — it folds together already-final `scope` outputs, which is new
|
|
87
|
+
* coordination logic no single owner has visibility to express itself.
|
|
88
|
+
*/
|
|
89
|
+
function worstScope(...scopes) {
|
|
90
|
+
return scopes.reduce((worst, s) => (SCOPE_SEVERITY[s] > SCOPE_SEVERITY[worst] ? s : worst));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Build one `PhaseSnapshot` for a single already-enumerated phase directory
|
|
94
|
+
* name. `isPhaseComplete` and `scanPhasePlans` each perform their own raw
|
|
95
|
+
* `readdirSync` against `fullPhaseDir` and can independently degrade — see
|
|
96
|
+
* the design doc's "Scope combination" section for why the two are genuinely
|
|
97
|
+
* uncorrelated (isPhaseComplete's readability check never re-derives or
|
|
98
|
+
* requires scanPhasePlans, and vice versa).
|
|
99
|
+
*/
|
|
100
|
+
function buildPhaseSnapshot(phasesDir, dir) {
|
|
101
|
+
const fullPhaseDir = node_path_1.default.join(phasesDir, dir);
|
|
102
|
+
const completionResult = isPhaseComplete(fullPhaseDir);
|
|
103
|
+
const scanResult = scanPhasePlans(fullPhaseDir);
|
|
104
|
+
return {
|
|
105
|
+
dir,
|
|
106
|
+
complete: completionResult.value.complete,
|
|
107
|
+
verificationStatus: completionResult.value.verification.status,
|
|
108
|
+
planCount: scanResult.planCount,
|
|
109
|
+
summaryCount: scanResult.summaryCount,
|
|
110
|
+
scope: worstScope(completionResult.scope, scanResult.scope),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolve every STATE.md-sourced field in one place: `currentPhaseLabel` (the
|
|
115
|
+
* raw `Phase:` field under `## Current Position`, e.g. `"3 of 8 (User
|
|
116
|
+
* Auth)"`, not a normalized phase-directory id — see the design doc's Known
|
|
117
|
+
* limits), `statePhaseTokens` (Phase 11, #3309 — every phase-number-shaped
|
|
118
|
+
* token found anywhere in STATE.md's raw text, backs W002), and `stateStatus`
|
|
119
|
+
* (Phase 11, #3309 — the `status`/`Status` field, backs W011).
|
|
120
|
+
*
|
|
121
|
+
* Phase 10 shipped `currentPhaseLabel` as its own single-purpose reader
|
|
122
|
+
* (`buildCurrentPhaseLabel(statePath)`); this phase folds two more STATE.md
|
|
123
|
+
* derivations in rather than reading and parsing the same file three times
|
|
124
|
+
* per `buildPlanningSnapshot` call — the read, `extractFrontmatter`, and
|
|
125
|
+
* `stripFrontmatter` are genuinely shared inputs for all three, and sharing
|
|
126
|
+
* them means `warnUnusableInput(STATE_UNREADABLE)` also stays a single call
|
|
127
|
+
* site instead of a risk of tripling on one degraded read.
|
|
128
|
+
*
|
|
129
|
+
* This module performs the one STATE.md read no §7 owner does, mirroring
|
|
130
|
+
* every existing STATE.md caller (`cmdStateSnapshot`, `cmdStatePrune`):
|
|
131
|
+
* `platformReadSync` + `extractFrontmatter` + `stripFrontmatter`.
|
|
132
|
+
*
|
|
133
|
+
* - STATE.md absent (ENOENT, `platformReadSync` returns `null`) is a real
|
|
134
|
+
* non-answer, NOT corruption — a project that never ran `state.init`
|
|
135
|
+
* legitimately has no STATE.md yet. `warnUnusableInput` is NOT called.
|
|
136
|
+
* - STATE.md present but unreadable (any other read error, e.g. EISDIR) is
|
|
137
|
+
* corruption — `warnUnusableInput(STATE_UNREADABLE)` fires exactly once,
|
|
138
|
+
* and all three fields degrade to their UNREADABLE non-answer together.
|
|
139
|
+
* - An unterminated frontmatter fence is reported by `extractFrontmatter`
|
|
140
|
+
* itself (`FRONTMATTER_UNTERMINATED`) — this function does not duplicate
|
|
141
|
+
* that diagnostic; it still attempts a body-only field read on whatever
|
|
142
|
+
* `stripFrontmatter` leaves behind.
|
|
143
|
+
* - `currentPhaseLabel`/`stateStatus` both live under `## Current Position`
|
|
144
|
+
* (`gsd-core/templates/state.md`) and both use `stateFieldValue`
|
|
145
|
+
* (`state-document.cts:296`) the exact way `smart-entry.cts:448`/
|
|
146
|
+
* `state.cts:1561,3273` already call it for `'status'`/`'Status'` — so a
|
|
147
|
+
* missing `## Current Position` section degrades BOTH to `TRUNCATED` with
|
|
148
|
+
* a whole-body fallback, together.
|
|
149
|
+
* - `statePhaseTokens` scans the WHOLE document (`verify.cts`'s exact
|
|
150
|
+
* `PHASE_NUMBER_TOKEN_SOURCE` regex, relocated verbatim from
|
|
151
|
+
* `verify.cts:1731-1735`), not just the Current Position section, so it is
|
|
152
|
+
* NOT degraded to `TRUNCATED` by a missing section header — it stays
|
|
153
|
+
* `COMPLETE` whenever the file itself was read successfully.
|
|
154
|
+
*/
|
|
155
|
+
function buildStateFields(statePath) {
|
|
156
|
+
let content;
|
|
157
|
+
try {
|
|
158
|
+
content = (0, shell_command_projection_cjs_1.platformReadSync)(statePath);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
warnUnusableInput({ reason: UNUSABLE_REASON.STATE_UNREADABLE, source: statePath });
|
|
162
|
+
return {
|
|
163
|
+
currentPhaseLabel: { value: null, scope: SCOPE.UNREADABLE },
|
|
164
|
+
statePhaseTokens: { value: [], scope: SCOPE.UNREADABLE },
|
|
165
|
+
stateStatus: { value: null, scope: SCOPE.UNREADABLE },
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (content === null) {
|
|
169
|
+
return {
|
|
170
|
+
currentPhaseLabel: { value: null, scope: SCOPE.UNREADABLE },
|
|
171
|
+
statePhaseTokens: { value: [], scope: SCOPE.UNREADABLE },
|
|
172
|
+
stateStatus: { value: null, scope: SCOPE.UNREADABLE },
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const frontmatter = extractFrontmatter(content, statePath);
|
|
176
|
+
const body = stripFrontmatter(content);
|
|
177
|
+
const section = (0, state_document_cjs_1.stateCurrentPositionSlice)(body);
|
|
178
|
+
const currentPositionScope = section === null ? SCOPE.TRUNCATED : SCOPE.COMPLETE;
|
|
179
|
+
// #1760 fallback ladder — now a full mirror of `state.cts`'s
|
|
180
|
+
// `resolveStatePhase` (its three-source ladder at `state.cts:1494-1516`),
|
|
181
|
+
// including the frontmatter step that ladder leads with:
|
|
182
|
+
// 1. frontmatter `current_phase` scalar — the machine-readable key
|
|
183
|
+
// `gsd-tools state update` / `state begin-phase` persist via
|
|
184
|
+
// `syncStateFrontmatter` (`state.cts:2023`), so it takes PRIORITY over
|
|
185
|
+
// any body field (#3280: a body-only ladder left W011 structurally
|
|
186
|
+
// blind to the one format the product itself writes — a stale body
|
|
187
|
+
// `Phase:` remnant even SHADOWED the current frontmatter value).
|
|
188
|
+
// 2. the legacy bold `**Current Phase:**` field (what `verify.cts:2109-
|
|
189
|
+
// 2111` originally matched, and what pre-template-migration STATE.md
|
|
190
|
+
// fixtures still use).
|
|
191
|
+
// 3. the current template's bare `Phase: [X] of [Y]` field.
|
|
192
|
+
// A document carrying several is read the same way `resolveStatePhase`
|
|
193
|
+
// reads it elsewhere — frontmatter first, then body, in that order.
|
|
194
|
+
const frontmatterCurrentPhase = (0, state_document_cjs_1.stateFieldValue)(frontmatter, body, 'current_phase', null);
|
|
195
|
+
const legacyCurrentPhaseLabel = (0, state_document_cjs_1.stateFieldValue)(frontmatter, section ?? body, null, 'Current Phase', {
|
|
196
|
+
scope: currentPositionScope,
|
|
197
|
+
});
|
|
198
|
+
const templateCurrentPhaseLabel = (0, state_document_cjs_1.stateFieldValue)(frontmatter, section ?? body, null, 'Phase', {
|
|
199
|
+
scope: currentPositionScope,
|
|
200
|
+
});
|
|
201
|
+
const currentPhaseLabel = {
|
|
202
|
+
value: frontmatterCurrentPhase.value ?? legacyCurrentPhaseLabel.value ?? templateCurrentPhaseLabel.value,
|
|
203
|
+
scope: frontmatterCurrentPhase.value !== null
|
|
204
|
+
? frontmatterCurrentPhase.scope
|
|
205
|
+
: legacyCurrentPhaseLabel.value !== null
|
|
206
|
+
? legacyCurrentPhaseLabel.scope
|
|
207
|
+
: templateCurrentPhaseLabel.scope,
|
|
208
|
+
};
|
|
209
|
+
const stateStatus = (0, state_document_cjs_1.stateFieldValue)(frontmatter, section ?? body, 'status', 'Status', {
|
|
210
|
+
scope: currentPositionScope,
|
|
211
|
+
});
|
|
212
|
+
const statePhaseTokens = {
|
|
213
|
+
value: [...content.matchAll(new RegExp(`[Pp]hase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})`, 'g'))].map((m) => m[1]),
|
|
214
|
+
scope: SCOPE.COMPLETE,
|
|
215
|
+
};
|
|
216
|
+
return { currentPhaseLabel, statePhaseTokens, stateStatus };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Resolve `config` — the parsed `.planning/config.json`, preserving the same
|
|
220
|
+
* three-way distinction `cmdValidateHealth` (`src/verify.cts` W003/E005)
|
|
221
|
+
* already makes without going through `loadConfig` (which collapses that
|
|
222
|
+
* distinction): absent is a real non-answer — `{value: null, scope:
|
|
223
|
+
* UNREADABLE, exists: false}`, no `warnUnusableInput` call, mirrors
|
|
224
|
+
* `buildCurrentPhaseLabel`'s treatment of an absent STATE.md; present but
|
|
225
|
+
* unparseable JSON IS corruption — `{value: null, scope: UNREADABLE, exists:
|
|
226
|
+
* true}`, `warnUnusableInput(CONFIG_UNREADABLE)` fires exactly once, so a
|
|
227
|
+
* later health-diagnostic rule can tell "config.json not found" (W003,
|
|
228
|
+
* repairable via `createConfig`) apart from "config.json: JSON parse error"
|
|
229
|
+
* (E005, repairable via `resetConfig`) — the `exists` flag is exactly that
|
|
230
|
+
* discriminator. `config.json` is root-scoped (`planningRoot`), NOT
|
|
231
|
+
* workstream-scoped (`planningPaths(cwd).config` would resolve under
|
|
232
|
+
* `.planning/workstreams/<ws>/` instead) — see verify.cts's own
|
|
233
|
+
* rootBase-vs-wsBase split at cmdValidateHealth's top.
|
|
234
|
+
*/
|
|
235
|
+
function buildConfigField(cwd) {
|
|
236
|
+
const configPath = node_path_1.default.join(planningRoot(cwd), 'config.json');
|
|
237
|
+
if (!node_fs_1.default.existsSync(configPath)) {
|
|
238
|
+
return { value: null, scope: SCOPE.UNREADABLE, exists: false };
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const raw = node_fs_1.default.readFileSync(configPath, 'utf-8');
|
|
242
|
+
const parsed = JSON.parse(raw);
|
|
243
|
+
return { value: parsed, scope: SCOPE.COMPLETE, exists: true };
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
warnUnusableInput({ reason: UNUSABLE_REASON.CONFIG_UNREADABLE, source: configPath });
|
|
247
|
+
return { value: null, scope: SCOPE.UNREADABLE, exists: true };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Resolve `agentInstall` — wraps `checkAgentsInstalled(runtime, cwd)` with
|
|
252
|
+
* the same `runtime` `cmdValidateHealth` resolves (`resolveRuntime(cwd)`,
|
|
253
|
+
* its `_slashRuntime`). Not `.planning/`-sourced (see design doc). `scope`
|
|
254
|
+
* is `COMPLETE` whenever the scan itself ran, even when it reports missing
|
|
255
|
+
* or incomplete agents — that is a real answer, not a non-answer.
|
|
256
|
+
* `UNREADABLE` only if the scan itself throws, mirroring cmdValidateHealth's
|
|
257
|
+
* own try/catch around this same call (there, the exception is swallowed as
|
|
258
|
+
* "non-blocking"; here it is surfaced via `scope` instead of silently
|
|
259
|
+
* dropped, since a snapshot field has nowhere else to carry that fact).
|
|
260
|
+
*/
|
|
261
|
+
function buildAgentInstallField(cwd) {
|
|
262
|
+
const runtime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
|
|
263
|
+
try {
|
|
264
|
+
return { value: checkAgentsInstalled(runtime, cwd), scope: SCOPE.COMPLETE };
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return {
|
|
268
|
+
value: {
|
|
269
|
+
agents_installed: false,
|
|
270
|
+
missing_agents: [],
|
|
271
|
+
installed_agents: [],
|
|
272
|
+
incomplete_agents: [],
|
|
273
|
+
agents_dir: '',
|
|
274
|
+
agent_runtime: runtime,
|
|
275
|
+
},
|
|
276
|
+
scope: SCOPE.UNREADABLE,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Resolve `worktreeHealth` — wraps `inspectWorktreeHealth(cwd, { staleAfterMs
|
|
282
|
+
* }, deps)` with the exact same arguments `cmdValidateHealth` passes
|
|
283
|
+
* (`src/verify.cts` W017/W020/W027 call sites): a 1-hour staleness window,
|
|
284
|
+
* and the raw `execGit`/`fs.existsSync`/`fs.statSync` seam (not
|
|
285
|
+
* `worktree-safety.cts`'s own `execGitDefault` wrapper). Not
|
|
286
|
+
* `.planning/`-sourced (see design doc). `scope` is `COMPLETE` only when the
|
|
287
|
+
* underlying `git worktree list` scan itself succeeded (`ok: true`) — a
|
|
288
|
+
* timed-out or failed scan (`ok: false`, mirroring W020's degraded-check
|
|
289
|
+
* report) or a thrown exception (mirrors cmdValidateHealth's own
|
|
290
|
+
* "git worktree not available or not a git repo — skip silently" catch)
|
|
291
|
+
* both degrade to `UNREADABLE` with an empty findings array, since neither
|
|
292
|
+
* case has real per-worktree data to report. `reason` carries
|
|
293
|
+
* `inspectWorktreeHealth`'s own discriminator ('ok' | 'git_timed_out' |
|
|
294
|
+
* 'git_list_failed' | 'not_a_git_repo') straight through — NOT discarded —
|
|
295
|
+
* so `checkW020` (`src/health-diagnostic-rules/worktree-health.cts`) can
|
|
296
|
+
* reproduce `verify.cts:2202-2217`'s exact branching: it warns on
|
|
297
|
+
* 'git_timed_out' or 'git_list_failed' but stays silent on 'not_a_git_repo'
|
|
298
|
+
* (a `.planning/`-only fixture/tmp dir with no git repo at all is not a
|
|
299
|
+
* degraded scan). A thrown exception reports 'exception', which also stays
|
|
300
|
+
* silent, matching the original's catch-all "skip silently" comment.
|
|
301
|
+
*/
|
|
302
|
+
function buildWorktreeHealthField(cwd) {
|
|
303
|
+
try {
|
|
304
|
+
const result = inspectWorktreeHealth(cwd, { staleAfterMs: 60 * 60 * 1000 }, { execGit: shell_command_projection_cjs_1.execGit, existsSync: node_fs_1.default.existsSync, statSync: node_fs_1.default.statSync });
|
|
305
|
+
if (!result.ok) {
|
|
306
|
+
return { value: [], scope: SCOPE.UNREADABLE, reason: result.reason };
|
|
307
|
+
}
|
|
308
|
+
return { value: result.findings, scope: SCOPE.COMPLETE, reason: result.reason };
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return { value: [], scope: SCOPE.UNREADABLE, reason: 'exception' };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// #3586 (Phase 2, epic #2292): matches `execGit`'s own default timeout
|
|
315
|
+
// (`shell-command-projection.cts:628`, also `10_000`) — kept as an explicit
|
|
316
|
+
// named constant here (rather than omitting `timeout` and relying on that
|
|
317
|
+
// default silently) so this call site's bound is self-documenting; generous
|
|
318
|
+
// enough for a normal repo, bounded enough to degrade rather than stall
|
|
319
|
+
// `buildPlanningSnapshot`, which every health path calls.
|
|
320
|
+
const PLANNING_TRACKED_GIT_TIMEOUT_MS = 10_000;
|
|
321
|
+
/**
|
|
322
|
+
* Resolve `planningTracked` — whether `.planning/` matches a gitignore rule
|
|
323
|
+
* AND whether at least one path under it is still tracked by git (#3586,
|
|
324
|
+
* Phase 2 of epic #2292). `.gitignore` has no effect on files git already
|
|
325
|
+
* tracks, so a project that committed `.planning/` before ignoring it keeps
|
|
326
|
+
* staging those files forever — while `commit_docs` auto-resolves `false`
|
|
327
|
+
* (`isGitIgnored`, reused below, is exactly what that resolution consults),
|
|
328
|
+
* which is what makes the contradiction invisible. Backs W029
|
|
329
|
+
* (`src/health-diagnostic-rules/config-validation.cts`).
|
|
330
|
+
*
|
|
331
|
+
* Modeled directly on `buildWorktreeHealthField` above (ADR-3180 §8.1 rule 1:
|
|
332
|
+
* a `Rule.check(snapshot)` may perform no ambient I/O, so this `git ls-files`
|
|
333
|
+
* probe lives here, in the snapshot builder, not the rule).
|
|
334
|
+
*
|
|
335
|
+
* `tracked` runs `git ls-files -- .planning` through the module's own
|
|
336
|
+
* injected `execGit` seam: non-empty stdout means at least one path under
|
|
337
|
+
* `.planning/` is in the INDEX — worktree presence is irrelevant (a path
|
|
338
|
+
* tracked in the index but deleted on disk still counts; that is what "the
|
|
339
|
+
* index is what matters" means for this probe).
|
|
340
|
+
*
|
|
341
|
+
* `ignored` reuses `isGitIgnored` (`config-loader.cjs`) — the SAME
|
|
342
|
+
* `git check-ignore -q --no-index` resolution `commit_docs` auto-resolution
|
|
343
|
+
* already calls (`config-loader.cts:824`) — rather than a second,
|
|
344
|
+
* independently-drifting `check-ignore` invocation. Only computed once the
|
|
345
|
+
* `ls-files` probe itself succeeded; a probe that could not run has no
|
|
346
|
+
* grounds to ask a second question either.
|
|
347
|
+
*
|
|
348
|
+
* Degradation mirrors `buildWorktreeHealthField` exactly: not a git repo →
|
|
349
|
+
* `SCOPE.UNREADABLE` + reason `not_a_git_repo` (silent downstream — matches
|
|
350
|
+
* the sibling builder's deliberate treatment of a `.planning/`-only
|
|
351
|
+
* fixture/tmp dir with no git repo at all); a timed-out `ls-files` →
|
|
352
|
+
* `git_timed_out`; any other non-zero exit → `git_list_failed`; a thrown
|
|
353
|
+
* exception → `exception`. Never throws out of the builder.
|
|
354
|
+
*
|
|
355
|
+
* Repo-presence is determined STRUCTURALLY, not by reading `ls-files`'s
|
|
356
|
+
* stderr prose (#3586): git localizes its error text (`LANG`/`LC_ALL`), so a
|
|
357
|
+
* regex matching the English "not a git repository" string silently
|
|
358
|
+
* misclassifies `not_a_git_repo` as `git_list_failed` under any non-English
|
|
359
|
+
* locale — this is exactly the "raw text matching on subprocess output"
|
|
360
|
+
* `CONTRIBUTING.md` bans, applied to production code rather than a test.
|
|
361
|
+
* Instead, on the `ls-files` failure path ONLY (never on the happy path —
|
|
362
|
+
* `buildPlanningSnapshot` runs on every health invocation, and the happy
|
|
363
|
+
* path must stay a single subprocess call), this asks git a structural
|
|
364
|
+
* yes/no question via `git rev-parse --is-inside-work-tree`: exit 0 means we
|
|
365
|
+
* ARE inside a work tree, so the `ls-files` failure was something else →
|
|
366
|
+
* `git_list_failed`; a non-zero exit means we are NOT → `not_a_git_repo`.
|
|
367
|
+
* If that probe itself times out, `result.timedOut` (the shared
|
|
368
|
+
* `isSpawnTimeout` predicate) reports `git_timed_out` — not a hand-rolled
|
|
369
|
+
* timeout check.
|
|
370
|
+
*
|
|
371
|
+
* `ENOBUFS` overflow (#3586 review F2): `execGit` sets no `maxBuffer`, so
|
|
372
|
+
* `spawnSync`'s Node-default 1MB cap applies to `ls-files`' stdout. A
|
|
373
|
+
* `.planning/` tree with enough tracked paths to exceed 1MB makes
|
|
374
|
+
* `spawnSync` report `error.code === 'ENOBUFS'` — exactly the large-tracked-
|
|
375
|
+
* history case this probe exists to catch, and exactly the case most likely
|
|
376
|
+
* to legitimately overflow the buffer. Falling into the generic
|
|
377
|
+
* non-zero-exit path here would misclassify it as `git_list_failed` →
|
|
378
|
+
* `SCOPE.UNREADABLE`, silently dropping the finding in precisely the
|
|
379
|
+
* scenario where it matters most. Overflow is therefore treated as PROOF OF
|
|
380
|
+
* TRACKING, not as a degraded read: `ls-files` only overflows because it had
|
|
381
|
+
* non-empty output to begin with, so `tracked` is unconditionally `true` —
|
|
382
|
+
* detected BEFORE the generic `exitCode !== 0` branch below, so this case
|
|
383
|
+
* never reaches (and never pays for) the `rev-parse` structural probe.
|
|
384
|
+
*/
|
|
385
|
+
function buildPlanningTrackedField(cwd) {
|
|
386
|
+
try {
|
|
387
|
+
const result = (0, shell_command_projection_cjs_1.execGit)(['ls-files', '--', '.planning'], { cwd, timeout: PLANNING_TRACKED_GIT_TIMEOUT_MS });
|
|
388
|
+
if (result.timedOut) {
|
|
389
|
+
return { value: { tracked: false, ignored: false }, scope: SCOPE.UNREADABLE, reason: 'git_timed_out' };
|
|
390
|
+
}
|
|
391
|
+
if (result.error?.code === 'ENOBUFS') {
|
|
392
|
+
const ignored = isGitIgnored(cwd, '.planning/');
|
|
393
|
+
return { value: { tracked: true, ignored }, scope: SCOPE.COMPLETE, reason: 'ok_truncated' };
|
|
394
|
+
}
|
|
395
|
+
if (result.exitCode !== 0) {
|
|
396
|
+
const probe = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--is-inside-work-tree'], { cwd, timeout: PLANNING_TRACKED_GIT_TIMEOUT_MS });
|
|
397
|
+
if (probe.timedOut) {
|
|
398
|
+
return { value: { tracked: false, ignored: false }, scope: SCOPE.UNREADABLE, reason: 'git_timed_out' };
|
|
399
|
+
}
|
|
400
|
+
const reason = probe.exitCode === 0 ? 'git_list_failed' : 'not_a_git_repo';
|
|
401
|
+
return { value: { tracked: false, ignored: false }, scope: SCOPE.UNREADABLE, reason };
|
|
402
|
+
}
|
|
403
|
+
const tracked = result.stdout.trim().length > 0;
|
|
404
|
+
const ignored = isGitIgnored(cwd, '.planning/');
|
|
405
|
+
return { value: { tracked, ignored }, scope: SCOPE.COMPLETE, reason: 'ok' };
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
return { value: { tracked: false, ignored: false }, scope: SCOPE.UNREADABLE, reason: 'exception' };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// ─── Phase 11 (#3309) "Rule table organization" builders ────────────────────
|
|
412
|
+
// Each relocates (not reinvents) an existing `verify.cts` derivation. See the
|
|
413
|
+
// design doc's "Rule table organization" table for the exact source lines.
|
|
414
|
+
/**
|
|
415
|
+
* Resolve `projectSections` — the `##`-level section headings actually
|
|
416
|
+
* present in `.planning/PROJECT.md`, as a plain list (NOT filtered against a
|
|
417
|
+
* required-sections list — the caller, the future W001/E002 rules, do that
|
|
418
|
+
* comparison). Relocates the read+parse half of `verify.cts:1681-1691`
|
|
419
|
+
* (E002/W001), generalized from "does the file include these three fixed
|
|
420
|
+
* strings" to "what headings does the file actually have."
|
|
421
|
+
*
|
|
422
|
+
* PROJECT.md is root-scoped (`planningRoot(cwd)`), NOT workstream-scoped —
|
|
423
|
+
* mirrors `cmdValidateHealth`'s own `projectPath = path.join(rootBase,
|
|
424
|
+
* 'PROJECT.md')` (`verify.cts:1649`), the same root-vs-workstream split
|
|
425
|
+
* `buildConfigField` already documents for config.json.
|
|
426
|
+
*
|
|
427
|
+
* Same `exists`-discriminator shape as `config`: absent file is a real
|
|
428
|
+
* non-answer (`{value: null, scope: UNREADABLE, exists: false}`, no
|
|
429
|
+
* `warnUnusableInput`); present but unreadable IS corruption —
|
|
430
|
+
* `{value: null, scope: UNREADABLE, exists: true}`,
|
|
431
|
+
* `warnUnusableInput(PROJECT_UNREADABLE)` fires exactly once, mirroring
|
|
432
|
+
* `buildConfigField`'s treatment of a present-but-unparseable config.json.
|
|
433
|
+
*/
|
|
434
|
+
function buildProjectSectionsField(cwd) {
|
|
435
|
+
const projectPath = node_path_1.default.join(planningRoot(cwd), 'PROJECT.md');
|
|
436
|
+
if (!node_fs_1.default.existsSync(projectPath)) {
|
|
437
|
+
return { value: null, scope: SCOPE.UNREADABLE, exists: false };
|
|
438
|
+
}
|
|
439
|
+
let content;
|
|
440
|
+
try {
|
|
441
|
+
content = node_fs_1.default.readFileSync(projectPath, 'utf-8');
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
warnUnusableInput({ reason: UNUSABLE_REASON.PROJECT_UNREADABLE, source: projectPath });
|
|
445
|
+
return { value: null, scope: SCOPE.UNREADABLE, exists: true };
|
|
446
|
+
}
|
|
447
|
+
const value = [...content.matchAll(/^##\s+(.+)$/gm)].map((m) => m[1].trim());
|
|
448
|
+
return { value, scope: SCOPE.COMPLETE, exists: true };
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Resolve `roadmapDeclaredPhases` — every phase id ROADMAP.md declares
|
|
452
|
+
* (heading-style AND checklist-style, not filtered to disk presence), each
|
|
453
|
+
* paired with the milestone-version section it was found under (`null` when
|
|
454
|
+
* found outside any versioned section). Backs W006/W007 (declared-phase
|
|
455
|
+
* half) and W021(2288)/W026(2392) (milestone-attribution half).
|
|
456
|
+
*
|
|
457
|
+
* The declared-phase-id half reuses `buildRoadmapPhaseVariants`
|
|
458
|
+
* (`validate.cts:136`, already imported by `verify.cts:12` — genuine existing
|
|
459
|
+
* reuse). The milestone-attribution half relocates
|
|
460
|
+
* `checkMilestonePrefixMismatches`'s `sectionRx`-based section walk
|
|
461
|
+
* (`verify.cts:1429-1459`, local/unexported there), generalized from "record
|
|
462
|
+
* only the mismatches" to "record every attribution" — this field exposes
|
|
463
|
+
* the parsed fact; the future W021/W026 rules make the mismatch judgment.
|
|
464
|
+
*/
|
|
465
|
+
function buildRoadmapDeclaredPhasesField(roadmapPath) {
|
|
466
|
+
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
467
|
+
return { value: [], scope: SCOPE.UNREADABLE };
|
|
468
|
+
}
|
|
469
|
+
let content;
|
|
470
|
+
try {
|
|
471
|
+
content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
return { value: [], scope: SCOPE.UNREADABLE };
|
|
475
|
+
}
|
|
476
|
+
const { roadmapPhases } = (0, validate_cjs_1.buildRoadmapPhaseVariants)(content);
|
|
477
|
+
const milestoneByPhase = new Map();
|
|
478
|
+
const sectionRx = /^#{1,3}\s+(?:\[[^\]]{1,200}\]\s*)?.*v(\d+\.\d+)/gim;
|
|
479
|
+
const sections = [];
|
|
480
|
+
let sm;
|
|
481
|
+
while ((sm = sectionRx.exec(content)) !== null) {
|
|
482
|
+
if (sections.length > 0)
|
|
483
|
+
sections[sections.length - 1].end = sm.index;
|
|
484
|
+
sections.push({ version: `v${sm[1]}`, start: sm.index, end: content.length });
|
|
485
|
+
}
|
|
486
|
+
const phaseRx = /#{2,4}\s*(?:\[[^\]]{1,200}\]\s*)?Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]{0,200}\))?\s*:/gi;
|
|
487
|
+
for (const section of sections) {
|
|
488
|
+
const sectionContent = content.slice(section.start, section.end);
|
|
489
|
+
phaseRx.lastIndex = 0;
|
|
490
|
+
let pm;
|
|
491
|
+
while ((pm = phaseRx.exec(sectionContent)) !== null) {
|
|
492
|
+
if (!milestoneByPhase.has(pm[1]))
|
|
493
|
+
milestoneByPhase.set(pm[1], section.version);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const value = [...roadmapPhases].map((phaseId) => ({
|
|
497
|
+
phaseId,
|
|
498
|
+
milestone: milestoneByPhase.get(phaseId) ?? null,
|
|
499
|
+
}));
|
|
500
|
+
return { value, scope: SCOPE.COMPLETE };
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Resolve `roadmapPhaseCheckboxes` — parsed `[x]`/`[ ]` checkbox state per
|
|
504
|
+
* phase from ROADMAP.md's progress-table region, keyed by phase id. Backs
|
|
505
|
+
* W011.
|
|
506
|
+
*
|
|
507
|
+
* Relocates and generalizes `verify.cts`'s W011 block (`verify.cts:2104-
|
|
508
|
+
* 2134`): that call site builds ONE hardcoded `phaseCheckboxRe` testing a
|
|
509
|
+
* single target phase id (STATE's current phase) for a `[x]` match. This
|
|
510
|
+
* builder is the same regex shape, generalized to CAPTURE both the check
|
|
511
|
+
* character and the phase id instead of interpolating one fixed target, so
|
|
512
|
+
* every declared checkbox is recorded, not just one.
|
|
513
|
+
*
|
|
514
|
+
* NOT a re-derivation of `isPhaseComplete` (`verification.cts:557`, ADR-3180
|
|
515
|
+
* §7.4, disk-strict): that owner explicitly refuses to consult the ROADMAP
|
|
516
|
+
* checkbox at all when DECIDING phase completion (`verification.cts:536-
|
|
517
|
+
* 537`). This field only exposes what the checkbox literally says, for a
|
|
518
|
+
* diagnostic (W011) whose entire purpose is flagging when the two DISAGREE —
|
|
519
|
+
* reading the data is not re-litigating who is authoritative.
|
|
520
|
+
*/
|
|
521
|
+
function buildRoadmapPhaseCheckboxesField(roadmapPath) {
|
|
522
|
+
if (!node_fs_1.default.existsSync(roadmapPath)) {
|
|
523
|
+
return { value: {}, scope: SCOPE.UNREADABLE };
|
|
524
|
+
}
|
|
525
|
+
let content;
|
|
526
|
+
try {
|
|
527
|
+
content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
return { value: {}, scope: SCOPE.UNREADABLE };
|
|
531
|
+
}
|
|
532
|
+
const checkboxRe = new RegExp(`-\\s*\\[([xX ])\\].*?Phase\\s+0*(${PHASE_NUMBER_TOKEN_SOURCE})${OPTIONAL_PHASE_TAG_SOURCE}[:\\s]`, 'gi');
|
|
533
|
+
const value = {};
|
|
534
|
+
let m;
|
|
535
|
+
while ((m = checkboxRe.exec(content)) !== null) {
|
|
536
|
+
value[m[2]] = m[1].toLowerCase() === 'x';
|
|
537
|
+
}
|
|
538
|
+
return { value, scope: SCOPE.COMPLETE };
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Resolve `researchValidationStatus` — per phase directory, whether its
|
|
542
|
+
* `*-RESEARCH.md` contains the literal heading `## Validation Architecture`,
|
|
543
|
+
* and whether a `*-VALIDATION.md` file exists in the same directory. Backs
|
|
544
|
+
* W009.
|
|
545
|
+
*
|
|
546
|
+
* Relocates the file-naming convention `verify.cts:1967-1990` (W009) uses to
|
|
547
|
+
* find "the" RESEARCH.md / VALIDATION.md in a phase dir: a flat,
|
|
548
|
+
* non-recursive `readdirSync` of the phase dir, then the first entry whose
|
|
549
|
+
* name ends `-RESEARCH.md` / any entry ending `-VALIDATION.md`. Computed for
|
|
550
|
+
* EVERY phase dir unconditionally (verify.cts's W009 only reads RESEARCH.md
|
|
551
|
+
* when `hasResearch && !hasValidation`; this field exposes both booleans
|
|
552
|
+
* regardless, so the future W009 rule does its own `hasResearch &&
|
|
553
|
+
* hasValidationArchitecture && !hasValidationMd` check against parsed data,
|
|
554
|
+
* not raw text).
|
|
555
|
+
*
|
|
556
|
+
* `scope` mirrors `phaseDirs.scope` (the caller-supplied enumeration): a
|
|
557
|
+
* per-directory read failure degrades that single entry's booleans to
|
|
558
|
+
* `false` and is silently skipped, mirroring `verify.cts`'s own
|
|
559
|
+
* `catch { intentionally empty }` around this exact read — this is a
|
|
560
|
+
* deliberate fail-open match to the pre-migration behavior, not a scope
|
|
561
|
+
* degradation, since the original never surfaced these failures either.
|
|
562
|
+
*/
|
|
563
|
+
function buildResearchValidationStatusField(phasesDir, phaseDirNames, enumerationScope) {
|
|
564
|
+
const value = phaseDirNames.map((dir) => {
|
|
565
|
+
const fullPhaseDir = node_path_1.default.join(phasesDir, dir);
|
|
566
|
+
let files;
|
|
567
|
+
try {
|
|
568
|
+
files = node_fs_1.default.readdirSync(fullPhaseDir);
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
return { dir, hasValidationArchitecture: false, hasValidationMd: false };
|
|
572
|
+
}
|
|
573
|
+
// #3511: scope the raw listing to this phase dir before the two
|
|
574
|
+
// phase-numbered-artifact predicates, so a stray cross-phase
|
|
575
|
+
// -RESEARCH.md/-VALIDATION.md sitting in the wrong directory cannot flip
|
|
576
|
+
// this phase's flags — mirrors core-utils.cts's getPhaseFileStats.
|
|
577
|
+
const scopedFiles = scopeToPhase(files, dir);
|
|
578
|
+
const researchFile = scopedFiles.find((f) => f.endsWith('-RESEARCH.md'));
|
|
579
|
+
const hasValidationMd = scopedFiles.some((f) => f.endsWith('-VALIDATION.md'));
|
|
580
|
+
let hasValidationArchitecture = false;
|
|
581
|
+
if (researchFile) {
|
|
582
|
+
try {
|
|
583
|
+
const researchContent = node_fs_1.default.readFileSync(node_path_1.default.join(fullPhaseDir, researchFile), 'utf-8');
|
|
584
|
+
hasValidationArchitecture = researchContent.includes('## Validation Architecture');
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
/* intentionally empty — mirrors verify.cts:1986-1988's own silent skip */
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return { dir, hasValidationArchitecture, hasValidationMd };
|
|
591
|
+
});
|
|
592
|
+
return { value, scope: enumerationScope };
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Resolve `milestoneArchiveStatus` — `archivedVersions` (versions with a
|
|
596
|
+
* `milestones/<ver>-ROADMAP.md` snapshot file present) and `documentedVersions`
|
|
597
|
+
* (`## <version>` headings already present in MILESTONES.md). Backs W018.
|
|
598
|
+
*
|
|
599
|
+
* Relocates `verify.cts:2301-2335` (W018)'s directory-scan glob
|
|
600
|
+
* (`^(v\d+\.\d+(?:\.\d+)?)-ROADMAP\.md$` against a flat, non-recursive
|
|
601
|
+
* `readdirSync` of `.planning/milestones/`) and its MILESTONES.md
|
|
602
|
+
* heading-membership check, generalized from "is THIS archived version's
|
|
603
|
+
* heading present" to "list every `## <version>` heading MILESTONES.md has."
|
|
604
|
+
*
|
|
605
|
+
* Confirmed NOT a fit for `listArchiveVersionDirs`
|
|
606
|
+
* (`phase-locator.cts:127`): that function scans `milestones/*-phases/`
|
|
607
|
+
* DIRECTORIES, a different target than this field's `milestones/*-ROADMAP.md`
|
|
608
|
+
* FILES — reusing it here would silently answer the wrong question.
|
|
609
|
+
*
|
|
610
|
+
* Root-scoped (`planningRoot(cwd)`), matching `verify.cts`'s own
|
|
611
|
+
* `rootBase`-based `milestonesPath`/`milestonesArchiveDir`.
|
|
612
|
+
*/
|
|
613
|
+
function buildMilestoneArchiveStatusField(cwd) {
|
|
614
|
+
const rootBase = planningRoot(cwd);
|
|
615
|
+
const milestonesArchiveDir = node_path_1.default.join(rootBase, 'milestones');
|
|
616
|
+
const milestonesPath = node_path_1.default.join(rootBase, 'MILESTONES.md');
|
|
617
|
+
let archivedVersions = [];
|
|
618
|
+
let scope = SCOPE.COMPLETE;
|
|
619
|
+
if (node_fs_1.default.existsSync(milestonesArchiveDir)) {
|
|
620
|
+
try {
|
|
621
|
+
const archiveFiles = node_fs_1.default.readdirSync(milestonesArchiveDir);
|
|
622
|
+
archivedVersions = archiveFiles
|
|
623
|
+
.map((f) => f.match(/^(v\d+\.\d+(?:\.\d+)?)-ROADMAP\.md$/))
|
|
624
|
+
.filter((m) => m !== null)
|
|
625
|
+
.map((m) => m[1]);
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
scope = SCOPE.UNREADABLE;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
let documentedVersions = [];
|
|
632
|
+
if (node_fs_1.default.existsSync(milestonesPath)) {
|
|
633
|
+
try {
|
|
634
|
+
const registryContent = node_fs_1.default.readFileSync(milestonesPath, 'utf-8');
|
|
635
|
+
documentedVersions = [...registryContent.matchAll(/^##\s+(v\d+\.\d+(?:\.\d+)?)/gm)].map((m) => m[1]);
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
scope = worstScope(scope, SCOPE.UNREADABLE);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return { value: { archivedVersions, documentedVersions }, scope };
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Resolve `planningRootFiles` — plain listing of file (not directory) names
|
|
645
|
+
* directly under `.planning/` root. Backs W019.
|
|
646
|
+
*
|
|
647
|
+
* Pairs with the existing exported `isCanonicalPlanningFile` predicate
|
|
648
|
+
* (`artifacts.cts:43`) — but per the design doc, that predicate is called by
|
|
649
|
+
* the future W019 RULE per filename, not by this builder; this field only
|
|
650
|
+
* needs to BE the raw filename list.
|
|
651
|
+
*/
|
|
652
|
+
function buildPlanningRootFilesField(cwd) {
|
|
653
|
+
try {
|
|
654
|
+
const entries = node_fs_1.default.readdirSync(planningRoot(cwd), { withFileTypes: true });
|
|
655
|
+
return { value: entries.filter((e) => e.isFile()).map((e) => e.name), scope: SCOPE.COMPLETE };
|
|
656
|
+
}
|
|
657
|
+
catch {
|
|
658
|
+
return { value: [], scope: SCOPE.UNREADABLE };
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Resolve `allPhaseDirNames` — every directory name directly under the
|
|
663
|
+
* active `phases/` root, UNFILTERED by `listMilestonePhaseDirs`'s
|
|
664
|
+
* current-milestone-window membership test (unlike `phaseDirs`). Backs
|
|
665
|
+
* W007 (see the field's own doc comment on `PlanningSnapshot` for why
|
|
666
|
+
* `phaseDirs` cannot). An absent `phases/` root is a real empty, not a
|
|
667
|
+
* failure (mirrors `listMilestonePhaseDirs`'s own treatment); a present but
|
|
668
|
+
* unreadable root degrades to `UNREADABLE` with an empty list.
|
|
669
|
+
*/
|
|
670
|
+
function buildAllPhaseDirNamesField(phasesDir) {
|
|
671
|
+
if (!node_fs_1.default.existsSync(phasesDir))
|
|
672
|
+
return { value: [], scope: SCOPE.COMPLETE };
|
|
673
|
+
try {
|
|
674
|
+
const value = node_fs_1.default
|
|
675
|
+
.readdirSync(phasesDir, { withFileTypes: true })
|
|
676
|
+
.filter((e) => e.isDirectory())
|
|
677
|
+
.map((e) => e.name)
|
|
678
|
+
.sort();
|
|
679
|
+
return { value, scope: SCOPE.COMPLETE };
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
return { value: [], scope: SCOPE.UNREADABLE };
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Resolve `archivedPhaseTokens` — every phase-number token belonging to a
|
|
687
|
+
* directory directly under any `.planning/milestones/*-phases/` archive.
|
|
688
|
+
* Backs W002's archived-phase exemption (#3652); see the field's own doc
|
|
689
|
+
* comment on `PlanningSnapshot`. Mirrors `verify.cts`'s
|
|
690
|
+
* `forEachArchivedPhaseToken` + `listMilestoneArchiveDirs` exactly — same
|
|
691
|
+
* `MILESTONE_ARCHIVE_DIR_RE` archive-dir filter, same `PHASE_TOKEN_FROM_DIR_RE`
|
|
692
|
+
* per-entry match, same `stripProjectCodePrefix` normalization — just
|
|
693
|
+
* collecting into a value array instead of an `onPhase` callback. An absent
|
|
694
|
+
* `milestones/` dir is a real empty (no archives yet), not a failure; a
|
|
695
|
+
* present-but-unreadable per-archive-dir entry is silently skipped, mirroring
|
|
696
|
+
* `forEachArchivedPhaseToken`'s own per-directory `catch { /* absent/unreadable *\/ }`.
|
|
697
|
+
*/
|
|
698
|
+
function buildArchivedPhaseTokensField(planBase) {
|
|
699
|
+
const milestonesDir = node_path_1.default.join(planBase, 'milestones');
|
|
700
|
+
let archiveDirs;
|
|
701
|
+
try {
|
|
702
|
+
archiveDirs = node_fs_1.default
|
|
703
|
+
.readdirSync(milestonesDir, { withFileTypes: true })
|
|
704
|
+
.filter((e) => e.isDirectory() && validate_cjs_1.MILESTONE_ARCHIVE_DIR_RE.test(e.name))
|
|
705
|
+
.map((e) => node_path_1.default.join(milestonesDir, e.name));
|
|
706
|
+
}
|
|
707
|
+
catch (err) {
|
|
708
|
+
if (err.code === 'ENOENT')
|
|
709
|
+
return { value: [], scope: SCOPE.COMPLETE };
|
|
710
|
+
return { value: [], scope: SCOPE.UNREADABLE };
|
|
711
|
+
}
|
|
712
|
+
const value = [];
|
|
713
|
+
for (const archiveDir of archiveDirs) {
|
|
714
|
+
try {
|
|
715
|
+
const entries = node_fs_1.default.readdirSync(archiveDir, { withFileTypes: true });
|
|
716
|
+
for (const e of entries) {
|
|
717
|
+
if (!e.isDirectory())
|
|
718
|
+
continue;
|
|
719
|
+
const m = e.name.match(validate_cjs_1.PHASE_TOKEN_FROM_DIR_RE);
|
|
720
|
+
if (m)
|
|
721
|
+
value.push(stripProjectCodePrefix(m[1]));
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
/* archive dir absent/unreadable — mirrors forEachArchivedPhaseToken */
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return { value, scope: SCOPE.COMPLETE };
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Resolve `currentMilestoneRoadmapPhaseIds` — every phase-number token found
|
|
732
|
+
* in ROADMAP.md's content once scoped to the CURRENT milestone via
|
|
733
|
+
* `extractCurrentMilestone(content, cwd)`. Backs W026's archive-tolerant
|
|
734
|
+
* unstarted-phase scan; see the field's own doc comment on `PlanningSnapshot`
|
|
735
|
+
* for why `roadmapDeclaredPhases` cannot serve this. An absent/unreadable
|
|
736
|
+
* ROADMAP.md degrades to an empty list, mirroring every other
|
|
737
|
+
* ROADMAP-sourced field's absent-file handling.
|
|
738
|
+
*/
|
|
739
|
+
function buildCurrentMilestoneRoadmapPhaseIdsField(cwd, roadmapPath) {
|
|
740
|
+
if (!node_fs_1.default.existsSync(roadmapPath))
|
|
741
|
+
return { value: [], scope: SCOPE.UNREADABLE };
|
|
742
|
+
let content;
|
|
743
|
+
try {
|
|
744
|
+
content = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
return { value: [], scope: SCOPE.UNREADABLE };
|
|
748
|
+
}
|
|
749
|
+
const scoped = extractCurrentMilestone(content, cwd);
|
|
750
|
+
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal
|
|
751
|
+
// mirror of OPTIONAL_PHASE_TAG_SOURCE) — verbatim from `verify.cts:2366`.
|
|
752
|
+
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+(${PHASE_NUMBER_TOKEN_SOURCE})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:`, 'gi');
|
|
753
|
+
const value = [...scoped.matchAll(phasePattern)].map((m) => m[1]);
|
|
754
|
+
return { value, scope: SCOPE.COMPLETE };
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Resolve `perPhasePlanNumbering`/`perPhaseOrphanSummaries`/
|
|
758
|
+
* `perPhaseWaveMissingPlans` — Phase 12 (#3310, ADR-3180 §8.4), backing
|
|
759
|
+
* C002/C003/C004. One shared per-phase-directory scan serves all three
|
|
760
|
+
* fields (mirrors `buildStateFields`'s "one builder, several named outputs"
|
|
761
|
+
* convention above): each of the three questions below reads the exact same
|
|
762
|
+
* `scanPhasePlans(fullPhaseDir)` result, so scanning each phase directory
|
|
763
|
+
* three separate times (one function per field) would triple the
|
|
764
|
+
* `readdirSync`/frontmatter-read cost for zero behavioral gain — the three
|
|
765
|
+
* subjects are independent QUESTIONS, not independent SCANS.
|
|
766
|
+
*
|
|
767
|
+
* Enumerated over `allPhaseDirNames`, NOT `phaseDirs` (the
|
|
768
|
+
* current-milestone-windowed twin): the pre-migration `cmdValidateConsistency`
|
|
769
|
+
* (`verify.cts:1521-1608`) walks `collectPhaseRoots(planBase)`'s flat
|
|
770
|
+
* `phases/` root via a plain, unfiltered `readdirSync` — every phase
|
|
771
|
+
* directory on disk, not just the ones the current milestone window
|
|
772
|
+
* resolves as "in scope" — exactly the un-windowed shape `allPhaseDirNames`
|
|
773
|
+
* already exposes for W007 (see that field's own doc comment). Using the
|
|
774
|
+
* windowed `phaseDirs` here would silently narrow C002/C003/C004's coverage
|
|
775
|
+
* relative to the behavior being relocated. Disclosed fidelity note: this
|
|
776
|
+
* does NOT walk `collectPhaseRoots`'s second root (an active archived
|
|
777
|
+
* milestone's `<ver>-phases/` directory) — `allPhaseDirNames` is scoped to
|
|
778
|
+
* the flat `phases/` root only, the same scope every other
|
|
779
|
+
* `allPhaseDirNames`-sourced field already carries.
|
|
780
|
+
*
|
|
781
|
+
* QUESTION 1 — `perPhasePlanNumbering`: the sorted list of `-NN-PLAN.md`
|
|
782
|
+
* sequence numbers physically present (superseded or not — a retired plan
|
|
783
|
+
* still occupied a number), from `allPlanFiles` via the exact
|
|
784
|
+
* `/-(\d{2})-PLAN\.md$/` regex `verify.cts:1558` already uses. This field
|
|
785
|
+
* exposes the raw per-phase number list only; the future C002 rule computes
|
|
786
|
+
* the gap itself.
|
|
787
|
+
*
|
|
788
|
+
* QUESTION 2 — `perPhaseOrphanSummaries`: every SUMMARY.md with no matching
|
|
789
|
+
* LIVE PLAN.md, via `findOrphanSummaries(planFiles, summaryFiles)`
|
|
790
|
+
* (`core-utils.cjs`, `verify.cts:1584` — the same owner
|
|
791
|
+
* `src/health-diagnostic-rules/phase-structure.cts`'s I001 rule already
|
|
792
|
+
* consumes indirectly via `PhaseSnapshot.planCount`/`summaryCount`, for the
|
|
793
|
+
* INVERSE question). Uses the live (superseded-excluded) `planFiles`, not
|
|
794
|
+
* `allPlanFiles` — a superseded plan's summary is still an orphan.
|
|
795
|
+
*
|
|
796
|
+
* QUESTION 3 — `perPhaseWaveMissingPlans`: every LIVE plan (`planFiles`,
|
|
797
|
+
* same live set as Question 2 — a superseded plan legitimately carries no
|
|
798
|
+
* `wave`) whose frontmatter has no `wave` key, via `extractFrontmatter`,
|
|
799
|
+
* mirroring `verify.cts:1596-1603` exactly. A plan file that cannot be read
|
|
800
|
+
* is silently skipped, mirroring `cmdValidateConsistency`'s own outer
|
|
801
|
+
* `catch { intentionally empty }` (`verify.cts:1605-1607`) around this exact
|
|
802
|
+
* loop — a fail-open match to the pre-migration behavior, not a new scope
|
|
803
|
+
* degradation.
|
|
804
|
+
*/
|
|
805
|
+
function buildPerPhasePlanScanFields(phasesDir, phaseDirNames, enumerationScope) {
|
|
806
|
+
const planNumbering = [];
|
|
807
|
+
const orphanSummaries = [];
|
|
808
|
+
const waveMissingPlans = [];
|
|
809
|
+
for (const phaseDir of phaseDirNames) {
|
|
810
|
+
const fullPhaseDir = node_path_1.default.join(phasesDir, phaseDir);
|
|
811
|
+
const { allPlanFiles, planFiles, summaryFiles } = scanPhasePlans(fullPhaseDir);
|
|
812
|
+
const planNums = allPlanFiles
|
|
813
|
+
.map((p) => {
|
|
814
|
+
const m = p.match(/-(\d{2})-PLAN\.md$/);
|
|
815
|
+
return m ? parseInt(m[1], 10) : null;
|
|
816
|
+
})
|
|
817
|
+
.filter((n) => n !== null)
|
|
818
|
+
.sort((a, b) => a - b);
|
|
819
|
+
planNumbering.push({ phaseDir, planNums });
|
|
820
|
+
for (const orphan of findOrphanSummaries(planFiles, summaryFiles)) {
|
|
821
|
+
orphanSummaries.push({ phaseDir, orphanSummary: orphan });
|
|
822
|
+
}
|
|
823
|
+
for (const plan of planFiles) {
|
|
824
|
+
try {
|
|
825
|
+
const planFilePath = node_path_1.default.join(fullPhaseDir, plan);
|
|
826
|
+
const content = node_fs_1.default.readFileSync(planFilePath, 'utf-8');
|
|
827
|
+
const fmData = extractFrontmatter(content, planFilePath);
|
|
828
|
+
if (!fmData['wave'])
|
|
829
|
+
waveMissingPlans.push({ phaseDir, plan });
|
|
830
|
+
}
|
|
831
|
+
catch {
|
|
832
|
+
/* unreadable plan file — mirrors verify.cts:1605-1607's own silent skip */
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return {
|
|
837
|
+
perPhasePlanNumbering: { value: planNumbering, scope: enumerationScope },
|
|
838
|
+
perPhaseOrphanSummaries: { value: orphanSummaries, scope: enumerationScope },
|
|
839
|
+
perPhaseWaveMissingPlans: { value: waveMissingPlans, scope: enumerationScope },
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Build the full `.planning/` projection for `cwd`. Composes the six §7
|
|
844
|
+
* owners named in the design doc's "Owners consumed" table, plus (Phase 11,
|
|
845
|
+
* #3309) the three additive subject-surface fields `config`/`agentInstall`/
|
|
846
|
+
* `worktreeHealth` — no re-derivation, no new semantic answer beyond what
|
|
847
|
+
* their respective owners already compute. See the design doc for the
|
|
848
|
+
* behavior table and rejected alternatives.
|
|
849
|
+
*/
|
|
850
|
+
function buildPlanningSnapshot(cwd) {
|
|
851
|
+
const paths = planningPaths(cwd);
|
|
852
|
+
const milestone = getMilestoneInfo(cwd);
|
|
853
|
+
const phaseDirs = listMilestonePhaseDirs(paths.phases, { cwd });
|
|
854
|
+
const phasesValue = phaseDirs.value.map((dir) => buildPhaseSnapshot(paths.phases, dir));
|
|
855
|
+
const stateFields = buildStateFields(paths.state);
|
|
856
|
+
const allPhaseDirNames = buildAllPhaseDirNamesField(paths.phases);
|
|
857
|
+
const perPhasePlanScanFields = buildPerPhasePlanScanFields(paths.phases, allPhaseDirNames.value, allPhaseDirNames.scope);
|
|
858
|
+
return {
|
|
859
|
+
cwd: node_path_1.default.resolve(cwd),
|
|
860
|
+
milestone,
|
|
861
|
+
phaseDirs,
|
|
862
|
+
phases: {
|
|
863
|
+
value: phasesValue,
|
|
864
|
+
scope: worstScope(phaseDirs.scope, ...phasesValue.map((p) => p.scope)),
|
|
865
|
+
},
|
|
866
|
+
currentPhaseLabel: stateFields.currentPhaseLabel,
|
|
867
|
+
config: buildConfigField(cwd),
|
|
868
|
+
agentInstall: buildAgentInstallField(cwd),
|
|
869
|
+
worktreeHealth: buildWorktreeHealthField(cwd),
|
|
870
|
+
planningTracked: buildPlanningTrackedField(cwd),
|
|
871
|
+
projectSections: buildProjectSectionsField(cwd),
|
|
872
|
+
statePhaseTokens: stateFields.statePhaseTokens,
|
|
873
|
+
stateStatus: stateFields.stateStatus,
|
|
874
|
+
roadmapDeclaredPhases: buildRoadmapDeclaredPhasesField(paths.roadmap),
|
|
875
|
+
roadmapPhaseCheckboxes: buildRoadmapPhaseCheckboxesField(paths.roadmap),
|
|
876
|
+
researchValidationStatus: buildResearchValidationStatusField(paths.phases, phaseDirs.value, phaseDirs.scope),
|
|
877
|
+
milestoneArchiveStatus: buildMilestoneArchiveStatusField(cwd),
|
|
878
|
+
planningRootFiles: buildPlanningRootFilesField(cwd),
|
|
879
|
+
allPhaseDirNames,
|
|
880
|
+
archivedPhaseTokens: buildArchivedPhaseTokensField(paths.planning),
|
|
881
|
+
currentMilestoneRoadmapPhaseIds: buildCurrentMilestoneRoadmapPhaseIdsField(cwd, paths.roadmap),
|
|
882
|
+
perPhasePlanNumbering: perPhasePlanScanFields.perPhasePlanNumbering,
|
|
883
|
+
perPhaseOrphanSummaries: perPhasePlanScanFields.perPhaseOrphanSummaries,
|
|
884
|
+
perPhaseWaveMissingPlans: perPhasePlanScanFields.perPhaseWaveMissingPlans,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
module.exports = {
|
|
888
|
+
buildPlanningSnapshot,
|
|
889
|
+
worstScope,
|
|
890
|
+
};
|