@opengsd/gsd-core 1.9.1 → 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 +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +27 -3
- package/agents/gsd-debug-session-manager.md +11 -0
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +12 -10
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +46 -0
- package/agents/gsd-planner.md +49 -54
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +26 -73
- package/bin/install.js +1272 -1238
- 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 +2 -2
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/plan-phase.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 +1009 -115
- package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
- package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
- package/gsd-core/bin/lib/api-coverage.cjs +123 -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 +926 -202
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- 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 +608 -148
- 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 +507 -24
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +114 -38
- 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 +94 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +665 -99
- package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +76 -0
- package/gsd-core/bin/lib/config.cjs +22 -2
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +217 -40
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +239 -32
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
- 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-integration.cjs +13 -1
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1325 -169
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +805 -264
- 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 +160 -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-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
- package/gsd-core/bin/lib/io.cjs +38 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +821 -109
- 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 +507 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +258 -58
- package/gsd-core/bin/lib/phase.cjs +891 -156
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
- 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 +60 -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/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +405 -84
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
- 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/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
- package/gsd-core/bin/lib/smart-entry.cjs +154 -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 +424 -105
- package/gsd-core/bin/lib/state.cjs +1927 -401
- package/gsd-core/bin/lib/surface.cjs +35 -10
- 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 +20 -4
- package/gsd-core/bin/lib/uat.cjs +706 -64
- 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 +33 -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 +287 -20
- package/gsd-core/bin/lib/verify.cjs +368 -880
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
- package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
- package/gsd-core/bin/lib/workstream.cjs +8 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/debugger-techniques.md +255 -0
- 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/research-documentation-lookup.md +5 -3
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +8 -7
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- package/gsd-core/references/worktree-branch-check.md +2 -2
- 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/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +33 -70
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +74 -166
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +160 -95
- package/gsd-core/workflows/debug.md +16 -17
- package/gsd-core/workflows/diagnose-issues.md +56 -8
- 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/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- 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 +64 -7
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- 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 +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +103 -187
- 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/help/modes/full.md +6 -1
- package/gsd-core/workflows/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +7 -6
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +35 -47
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +27 -240
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +89 -209
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +45 -159
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +55 -405
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +41 -13
- package/gsd-core/workflows/section-manifest.json +219 -0
- 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/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +41 -20
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +106 -13
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +53 -31
- package/gsd-core/workflows/ui-phase.md +13 -12
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +19 -8
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +17 -65
- package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
- package/hooks/dist/gsd-check-update-worker.js +64 -12
- 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 +607 -26
- 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 +45 -24
- package/hooks/dist/gsd-statusline.js +90 -6
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- 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 +277 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +517 -0
- package/hooks/gsd-check-update-worker.js +64 -12
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +607 -26
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +45 -24
- package/hooks/gsd-statusline.js +90 -6
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- 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 +277 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +31 -10
- package/pi/gsd.cjs +71 -12
- 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/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +68 -6
- package/scripts/changeset/serialize.cjs +5 -1
- 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-matrix.cjs +1 -1
- package/scripts/gen-capability-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +439 -0
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +150 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- 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 +15 -54
- 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-compiled-artifact-sync.cjs +6 -1
- 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-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/lint-example-parser-parity.cjs +395 -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 +40 -3
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +52 -6
- 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 +2 -2
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +2 -2
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +1 -2
- 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 -577
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/gen-emitted-baseline.cjs +0 -145
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1050
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Plan Dependency Graph — shared halt-propagation over a plan's depends_on DAG (#2830).
|
|
4
|
+
*
|
|
5
|
+
* Two independent "which plans are incomplete" readers exist in this codebase:
|
|
6
|
+
* phase.cts's wave-grouping (`cmdPhasePlanIndex`) and phase-locator.cts's
|
|
7
|
+
* phase-location primitive (`searchPhaseInDir`, consumed by ~50 symbols across
|
|
8
|
+
* five command routers). Before #2830, only the former parsed `depends_on` —
|
|
9
|
+
* and even it used the DAG only for topological wave assignment, never to
|
|
10
|
+
* propagate a halted plan's block onto its dependents. The latter never parsed
|
|
11
|
+
* `depends_on` at all; it derived completion from summary-file presence only.
|
|
12
|
+
* A plan that reaches a designed stop still writes a SUMMARY (recording the
|
|
13
|
+
* halt in prose only — no prior structured status existed for it), so both
|
|
14
|
+
* readers saw it as ordinary "complete" and reported its dependents as
|
|
15
|
+
* ordinary incomplete work, available to spawn against.
|
|
16
|
+
*
|
|
17
|
+
* This module is the SINGLE topological-order + halt-propagation engine both
|
|
18
|
+
* readers call, so the two can never re-diverge on this rule again. Each
|
|
19
|
+
* caller resolves its own raw `depends_on` tokens to canonical plan ids
|
|
20
|
+
* (case-fold + `extractCanonicalPlanId` fallback — the same resolution
|
|
21
|
+
* already performed by phase.cts's `computeDependencyLevels`) before
|
|
22
|
+
* building `PlanHaltNode[]`; this module owns the graph traversal (exactly
|
|
23
|
+
* one pass — Kahn's algorithm, or none at all when the caller already has a
|
|
24
|
+
* valid topological order, see `computeHaltPropagation`'s `precomputedOrder`
|
|
25
|
+
* parameter) plus the two small pure helpers below (`isHaltedStatus`,
|
|
26
|
+
* `buildSummaryFileIndex`) that both callers would otherwise duplicate
|
|
27
|
+
* identically — the exact "two implementations, one drifts" failure mode
|
|
28
|
+
* this fix exists to close. Each caller still owns its own file I/O (the
|
|
29
|
+
* actual `fs.readFileSync` + `extractFrontmatter` calls); only the
|
|
30
|
+
* interpretive logic is centralized here — except the SUMMARY-file
|
|
31
|
+
* read+extract wrapper itself (`isSummaryFileHalted`), which both callers
|
|
32
|
+
* previously duplicated near-identically and which is now centralized here
|
|
33
|
+
* too, for the same reason.
|
|
34
|
+
*/
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
|
|
40
|
+
const frontmatterMod = require("./frontmatter.cjs");
|
|
41
|
+
const { extractFrontmatter } = frontmatterMod;
|
|
42
|
+
/**
|
|
43
|
+
* The one place "does this SUMMARY status value mean halted" is decided.
|
|
44
|
+
* Case-insensitive, trims whitespace. Both `phase.cts`'s `cmdPhasePlanIndex`
|
|
45
|
+
* and `phase-locator.cts`'s `searchPhaseInDir` call this after reading a
|
|
46
|
+
* completed plan's SUMMARY frontmatter `status` field, so the definition of
|
|
47
|
+
* "halted" cannot drift between the two readers.
|
|
48
|
+
*/
|
|
49
|
+
function isHaltedStatus(status) {
|
|
50
|
+
if (typeof status !== 'string')
|
|
51
|
+
return false;
|
|
52
|
+
// #2830 review (defect 2): strip an unquoted trailing YAML comment (a run
|
|
53
|
+
// of whitespace followed by `#` and the rest of the line) before
|
|
54
|
+
// trimming/lowercasing. YAML scalars don't need quoting to carry an inline
|
|
55
|
+
// comment (`status: halted # designed stop`), but `extractFrontmatter`
|
|
56
|
+
// does not strip one — and all four summary templates literally show that
|
|
57
|
+
// spelling as guidance on the value line. Without this, an executor that
|
|
58
|
+
// mimics the template's own presentation would write a halt that silently
|
|
59
|
+
// reads back as not-halted. A `#` with no preceding whitespace is NOT a
|
|
60
|
+
// YAML comment start, so `halted#nospace` intentionally still fails to match.
|
|
61
|
+
const withoutTrailingComment = status.replace(/\s+#.*$/, '');
|
|
62
|
+
return withoutTrailingComment.trim().toLowerCase() === 'halted';
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Read a plan's SUMMARY file and report whether it declares `status: halted`
|
|
66
|
+
* (a designed stop, not an ordinary completion). Returns false — never
|
|
67
|
+
* throws — on a missing/unreadable/malformed SUMMARY, so an unreadable file
|
|
68
|
+
* degrades to the pre-#2830 behavior ("has a SUMMARY = complete") rather
|
|
69
|
+
* than breaking either caller.
|
|
70
|
+
*
|
|
71
|
+
* Two callers share this wrapper: `phase.cts`'s `cmdPhasePlanIndex` and
|
|
72
|
+
* `phase-locator.cts`'s `searchPhaseInDir` (the phase-location primitive
|
|
73
|
+
* consumed by ~50 symbols across five command routers). Both previously
|
|
74
|
+
* carried a near-identical local copy (read file -> `extractFrontmatter` ->
|
|
75
|
+
* `isHaltedStatus` -> swallow errors) that this module's own header comment
|
|
76
|
+
* calls out as the exact "two implementations, one drifts" failure mode it
|
|
77
|
+
* exists to prevent — centralizing the read+extract wrapper here, not just
|
|
78
|
+
* the `isHaltedStatus` predicate, closes that gap.
|
|
79
|
+
*
|
|
80
|
+
* Takes a single resolved `summaryPath` (not a `dir` + `filename` pair) —
|
|
81
|
+
* `phase-locator.cts`'s prior local copy took the two parts separately and
|
|
82
|
+
* `path.join`'d them internally; that caller now does the join itself
|
|
83
|
+
* before calling in, so both callers share one signature.
|
|
84
|
+
*
|
|
85
|
+
* @param summaryPath - absolute or relative path to a `*-SUMMARY.md` file.
|
|
86
|
+
*/
|
|
87
|
+
function isSummaryFileHalted(summaryPath) {
|
|
88
|
+
try {
|
|
89
|
+
const content = node_fs_1.default.readFileSync(summaryPath, 'utf-8');
|
|
90
|
+
const fm = extractFrontmatter(content, summaryPath);
|
|
91
|
+
return isHaltedStatus(fm['status']);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build a planId -> summary-filename lookup from a phase's summary file
|
|
99
|
+
* list, keyed by both the exact SUMMARY-file-derived id and its canonical
|
|
100
|
+
* form (mirrors the `completedPlanIds` construction each caller already
|
|
101
|
+
* performs for its own SUMMARY-presence check — same `summaryFiles` list,
|
|
102
|
+
* same exact/canonical key pair — so the two can never disagree about which
|
|
103
|
+
* summary file belongs to which plan id). `extractCanonicalPlanId` is
|
|
104
|
+
* supplied by the caller (each module owns its own resolution helper).
|
|
105
|
+
*/
|
|
106
|
+
function buildSummaryFileIndex(summaryFiles, extractCanonicalPlanId) {
|
|
107
|
+
const index = new Map();
|
|
108
|
+
for (const s of summaryFiles) {
|
|
109
|
+
const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
110
|
+
const canonical = extractCanonicalPlanId(s);
|
|
111
|
+
index.set(exact, s);
|
|
112
|
+
if (canonical !== exact)
|
|
113
|
+
index.set(canonical, s);
|
|
114
|
+
}
|
|
115
|
+
return index;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* #3345: the one place "does this SUMMARY status value mean blocked" is
|
|
119
|
+
* decided, sibling to `isHaltedStatus` above. A SUMMARY declaring
|
|
120
|
+
* `status: blocked` records a plan that could NOT finish — it is a failure
|
|
121
|
+
* record, not a completion record — so it must not count toward
|
|
122
|
+
* `completed_plans` (scanPhasePlans's summaryCount) nor read as
|
|
123
|
+
* `has_summary: true` in phase-plan-index's `incomplete` construction.
|
|
124
|
+
*
|
|
125
|
+
* `halted` is deliberately NOT matched here: a designed stop still writes a
|
|
126
|
+
* completion record (#2830's model — its dependents get `blocked_by` halt
|
|
127
|
+
* propagation), so a `status: halted` SUMMARY keeps counting as summarized.
|
|
128
|
+
* Case-insensitive, trims whitespace, and strips an unquoted trailing YAML
|
|
129
|
+
* comment exactly like `isHaltedStatus` (same #2830 review defect-2 rule).
|
|
130
|
+
*/
|
|
131
|
+
function isBlockedStatus(status) {
|
|
132
|
+
if (typeof status !== 'string')
|
|
133
|
+
return false;
|
|
134
|
+
const withoutTrailingComment = status.replace(/\s+#.*$/, '');
|
|
135
|
+
return withoutTrailingComment.trim().toLowerCase() === 'blocked';
|
|
136
|
+
}
|
|
137
|
+
// #3345: SUMMARY frontmatter sits at byte 0 and closes well before the body,
|
|
138
|
+
// so only a bounded prefix is ever needed to read the `status` marker — the
|
|
139
|
+
// same cap discipline as plan-scan.cts's PLAN_FRONTMATTER_READ_CAP (#2349),
|
|
140
|
+
// kept here because this predicate's primary caller (scanPhasePlans) loops
|
|
141
|
+
// over every phase directory on hot paths (state sync, roadmap progress).
|
|
142
|
+
const SUMMARY_FRONTMATTER_READ_CAP = 64 * 1024;
|
|
143
|
+
/**
|
|
144
|
+
* #3345: read a SUMMARY file's frontmatter `status` (bounded-prefix read) and
|
|
145
|
+
* report whether it declares `status: blocked`. Returns false — never throws —
|
|
146
|
+
* on a missing/unreadable/malformed/non-regular SUMMARY, so an unreadable file
|
|
147
|
+
* degrades to the pre-#3345 filename-existence behaviour rather than breaking
|
|
148
|
+
* either caller. Fail-open by design, mirroring `isPlanSuperseded`'s posture.
|
|
149
|
+
*
|
|
150
|
+
* Callers: plan-scan.cts's `scanPhasePlans` (the count side) and phase.cts's
|
|
151
|
+
* `cmdPhasePlanIndex` (the read side) BOTH filter their summary lists through
|
|
152
|
+
* this one predicate, so the count and the `incomplete` list can never
|
|
153
|
+
* re-diverge on the blocked rule.
|
|
154
|
+
*/
|
|
155
|
+
function isSummaryFileBlocked(summaryPath) {
|
|
156
|
+
let content;
|
|
157
|
+
try {
|
|
158
|
+
const st = node_fs_1.default.statSync(summaryPath); // follows symlinks → resolves to the target's real type
|
|
159
|
+
if (!st.isFile())
|
|
160
|
+
return false;
|
|
161
|
+
const length = Math.min(st.size, SUMMARY_FRONTMATTER_READ_CAP);
|
|
162
|
+
if (length === 0)
|
|
163
|
+
return false;
|
|
164
|
+
const fd = node_fs_1.default.openSync(summaryPath, 'r');
|
|
165
|
+
try {
|
|
166
|
+
const buf = Buffer.allocUnsafe(length);
|
|
167
|
+
const bytesRead = node_fs_1.default.readSync(fd, buf, 0, length, 0);
|
|
168
|
+
content = buf.toString('utf8', 0, bytesRead);
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
node_fs_1.default.closeSync(fd);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
return isBlockedStatus(extractFrontmatter(content, summaryPath)['status']);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Computes halt-propagation over a plan dependency DAG.
|
|
181
|
+
*
|
|
182
|
+
* `precomputedOrder`: when the caller ALREADY has a valid topological order
|
|
183
|
+
* for these exact node ids (e.g. phase.cts's `cmdPhasePlanIndex`, which runs
|
|
184
|
+
* `computeDependencyLevels`'s Kahn's-algorithm pass for wave assignment
|
|
185
|
+
* before ever calling this function), pass it here and this function skips
|
|
186
|
+
* running Kahn's algorithm a second time — the halt-propagation forward pass
|
|
187
|
+
* below only needs A valid topological order, not to derive one itself.
|
|
188
|
+
* Omit it (as phase-locator.cts's `searchPhaseInDir` does — it has no prior
|
|
189
|
+
* traversal of this DAG) and this function derives the order itself; either
|
|
190
|
+
* way, exactly one Kahn's-algorithm pass runs per caller, never two.
|
|
191
|
+
*
|
|
192
|
+
* Diamond-safe (a plan blocked via two different halted ancestors gets both
|
|
193
|
+
* in its `blockedBy` list, deduplicated) and transitive-safe (a dependent of
|
|
194
|
+
* a dependent of a halted plan is blocked, at any depth).
|
|
195
|
+
*/
|
|
196
|
+
function computeHaltPropagation(nodes, precomputedOrder) {
|
|
197
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
198
|
+
let order;
|
|
199
|
+
let visited;
|
|
200
|
+
if (precomputedOrder) {
|
|
201
|
+
// Caller already ran Kahn's algorithm over this exact node set (same ids)
|
|
202
|
+
// — trust its order and skip re-deriving one. `visited` mirrors the same
|
|
203
|
+
// "count of nodes reachable in valid topological order" contract a fresh
|
|
204
|
+
// derivation would produce.
|
|
205
|
+
order = precomputedOrder;
|
|
206
|
+
visited = precomputedOrder.length;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const inDeg = new Map();
|
|
210
|
+
const adj = new Map(); // dependency id -> dependent ids
|
|
211
|
+
for (const n of nodes) {
|
|
212
|
+
if (!inDeg.has(n.id))
|
|
213
|
+
inDeg.set(n.id, 0);
|
|
214
|
+
if (!adj.has(n.id))
|
|
215
|
+
adj.set(n.id, []);
|
|
216
|
+
for (const depId of n.resolvedDependsOn) {
|
|
217
|
+
if (!byId.has(depId))
|
|
218
|
+
continue; // fail-safe: caller should have filtered these already
|
|
219
|
+
if (!adj.has(depId))
|
|
220
|
+
adj.set(depId, []);
|
|
221
|
+
adj.get(depId).push(n.id);
|
|
222
|
+
inDeg.set(n.id, (inDeg.get(n.id) ?? 0) + 1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const queue = [];
|
|
226
|
+
for (const n of nodes) {
|
|
227
|
+
if ((inDeg.get(n.id) ?? 0) === 0)
|
|
228
|
+
queue.push(n.id);
|
|
229
|
+
}
|
|
230
|
+
// Dequeue by head index, not Array.shift() — O(1) amortized, same
|
|
231
|
+
// rationale as computeDependencyLevels (#307).
|
|
232
|
+
let head = 0;
|
|
233
|
+
let v = 0;
|
|
234
|
+
while (head < queue.length) {
|
|
235
|
+
const cur = queue[head++];
|
|
236
|
+
v++;
|
|
237
|
+
for (const dep of adj.get(cur) ?? []) {
|
|
238
|
+
inDeg.set(dep, inDeg.get(dep) - 1);
|
|
239
|
+
if (inDeg.get(dep) === 0)
|
|
240
|
+
queue.push(dep);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
order = queue;
|
|
244
|
+
visited = v;
|
|
245
|
+
}
|
|
246
|
+
// `order` is a valid topological order for every visited node: for edge
|
|
247
|
+
// dep -> dependent, dep appears before dependent. A single forward pass
|
|
248
|
+
// over it — using each node's own already-resolved dependsOn ids —
|
|
249
|
+
// computes blockedBy without any further graph traversal.
|
|
250
|
+
const blockedBy = new Map();
|
|
251
|
+
for (const id of order) {
|
|
252
|
+
const node = byId.get(id);
|
|
253
|
+
if (!node)
|
|
254
|
+
continue;
|
|
255
|
+
const causes = new Set();
|
|
256
|
+
for (const depId of node.resolvedDependsOn) {
|
|
257
|
+
const depNode = byId.get(depId);
|
|
258
|
+
if (!depNode)
|
|
259
|
+
continue;
|
|
260
|
+
if (depNode.halted)
|
|
261
|
+
causes.add(depId);
|
|
262
|
+
const depCauses = blockedBy.get(depId);
|
|
263
|
+
if (depCauses) {
|
|
264
|
+
for (const c of depCauses)
|
|
265
|
+
causes.add(c);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (causes.size > 0)
|
|
269
|
+
blockedBy.set(id, Array.from(causes));
|
|
270
|
+
}
|
|
271
|
+
// #2830 review (defect 1): a node involved in a depends_on cycle (or
|
|
272
|
+
// downstream of one) never reaches indegree 0, so it never appears in
|
|
273
|
+
// `order` and the forward pass above never visits it — it would otherwise
|
|
274
|
+
// end up absent from BOTH `blockedBy` and any cycle diagnostic, i.e.
|
|
275
|
+
// reported as ordinary runnable. A node whose position in the topological
|
|
276
|
+
// order is undecidable cannot be shown to be safe to run: silently
|
|
277
|
+
// dropping it is the exact silent-disappearance failure #2830 exists to
|
|
278
|
+
// prevent. Fail closed — give every such non-halted node an explicit,
|
|
279
|
+
// non-empty `blockedBy` entry so no consumer (present or future) can
|
|
280
|
+
// re-admit it as runnable merely by checking "absent from blockedBy".
|
|
281
|
+
// (A node that is itself halted is not "blocked" — it IS the blocker —
|
|
282
|
+
// so it is left out here exactly as the normal forward pass leaves it out.)
|
|
283
|
+
if (order.length < nodes.length) {
|
|
284
|
+
const orderSet = new Set(order);
|
|
285
|
+
for (const n of nodes) {
|
|
286
|
+
if (orderSet.has(n.id) || n.halted)
|
|
287
|
+
continue;
|
|
288
|
+
const causes = Array.from(new Set(n.resolvedDependsOn.filter((depId) => byId.has(depId)))).sort();
|
|
289
|
+
blockedBy.set(n.id, causes.length > 0 ? causes : [n.id]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return { order, visited, blockedBy };
|
|
293
|
+
}
|
|
294
|
+
module.exports = {
|
|
295
|
+
computeHaltPropagation,
|
|
296
|
+
isHaltedStatus,
|
|
297
|
+
buildSummaryFileIndex,
|
|
298
|
+
isSummaryFileHalted,
|
|
299
|
+
// #3345: blocked-SUMMARY detection, shared by the count side (scanPhasePlans)
|
|
300
|
+
// and the read side (phase-plan-index) so the two cannot diverge.
|
|
301
|
+
isBlockedStatus,
|
|
302
|
+
isSummaryFileBlocked,
|
|
303
|
+
};
|
|
@@ -20,6 +20,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
20
20
|
exports.AUTHORITY_RUNGS = void 0;
|
|
21
21
|
exports.getEffectiveAuthority = getEffectiveAuthority;
|
|
22
22
|
exports.classifyDriftSeverity = classifyDriftSeverity;
|
|
23
|
+
exports.comparePhaseStatus = comparePhaseStatus;
|
|
23
24
|
/**
|
|
24
25
|
* Frozen map from authority name to its rung number.
|
|
25
26
|
*
|
|
@@ -115,3 +116,122 @@ function classifyDriftSeverity({ status, authority, }) {
|
|
|
115
116
|
return { severity: 'INFO', hardBlock: false };
|
|
116
117
|
}
|
|
117
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Frozen map from lowercased phase-status text to a shared ordinal rank,
|
|
121
|
+
* covering the union of the STATE.md "Current Position" vocabulary
|
|
122
|
+
* (gsd-core/templates/state.md) and the FULL ROADMAP.md "## Progress" table
|
|
123
|
+
* Status column vocabulary declared by gsd-core/templates/roadmap.md:133 —
|
|
124
|
+
* `Not started | In progress | Complete | Deferred`.
|
|
125
|
+
*
|
|
126
|
+
* The two vocabularies overlap on 'in progress', which is rank 1 in both —
|
|
127
|
+
* no conflict. 'deferred' is rank 0 (no work done) — see comparePhaseStatus's
|
|
128
|
+
* doc comment for how a deferred/non-rank-0 mismatch is classified; it is NOT
|
|
129
|
+
* simply numeric distance from rank 0 like an ordinary lag.
|
|
130
|
+
*/
|
|
131
|
+
const PHASE_STATUS_RANKS = Object.freeze({
|
|
132
|
+
// STATE.md "Current Position" vocabulary
|
|
133
|
+
'ready to plan': 0,
|
|
134
|
+
'planning': 0,
|
|
135
|
+
'ready to execute': 1,
|
|
136
|
+
'in progress': 1,
|
|
137
|
+
'phase complete': 2,
|
|
138
|
+
// ROADMAP.md "## Progress" table Status column vocabulary
|
|
139
|
+
'not started': 0,
|
|
140
|
+
'complete': 2,
|
|
141
|
+
'deferred': 0,
|
|
142
|
+
});
|
|
143
|
+
/** Rank at which a status asserts work is DONE (terminal, not comparative). */
|
|
144
|
+
const TERMINAL_RANK = 2;
|
|
145
|
+
/**
|
|
146
|
+
* Normalize a raw phase-status string for lookup/comparison: trims
|
|
147
|
+
* surrounding whitespace and lowercases. Returns null for missing/empty
|
|
148
|
+
* values. Single owner of this normalization so `resolvePhaseStatusRank` and
|
|
149
|
+
* the 'deferred' declared-intent check in `comparePhaseStatus` cannot drift
|
|
150
|
+
* apart on what counts as "empty".
|
|
151
|
+
*/
|
|
152
|
+
function normalizePhaseStatusText(value) {
|
|
153
|
+
if (value === null || value === undefined)
|
|
154
|
+
return null;
|
|
155
|
+
const normalized = value.trim().toLowerCase();
|
|
156
|
+
return normalized === '' ? null : normalized;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Resolve a raw phase-status string to its shared ordinal rank, or null when
|
|
160
|
+
* the value is missing/empty/unrecognized. Case-insensitive, trims
|
|
161
|
+
* surrounding whitespace.
|
|
162
|
+
*
|
|
163
|
+
* @param value - raw status text from STATE.md or ROADMAP.md
|
|
164
|
+
* @returns the resolved rank, or null if unresolvable
|
|
165
|
+
*/
|
|
166
|
+
function resolvePhaseStatusRank(value) {
|
|
167
|
+
const normalized = normalizePhaseStatusText(value);
|
|
168
|
+
if (normalized === null)
|
|
169
|
+
return null;
|
|
170
|
+
if (!Object.prototype.hasOwnProperty.call(PHASE_STATUS_RANKS, normalized))
|
|
171
|
+
return null;
|
|
172
|
+
return PHASE_STATUS_RANKS[normalized];
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Compare a phase's status as reported by STATE.md against the same phase's
|
|
176
|
+
* status as reported by ROADMAP.md's "## Progress" table, and classify the
|
|
177
|
+
* result.
|
|
178
|
+
*
|
|
179
|
+
* Unlike classifyDriftSeverity, this never throws for an unrecognized
|
|
180
|
+
* status: the inputs are user document text (prose a human or agent typed
|
|
181
|
+
* into STATE.md/ROADMAP.md), not config, so an unrecognized value is data —
|
|
182
|
+
* surfaced as 'uncheckable' — not a programming error.
|
|
183
|
+
*
|
|
184
|
+
* Rank 2 ('phase complete' / 'Complete') is TERMINAL: it asserts the work is
|
|
185
|
+
* DONE. If exactly one side reports rank 2 and the other does not, that is
|
|
186
|
+
* always 'drifted', regardless of numeric distance — a document claiming
|
|
187
|
+
* "done" while another claims "still going" is a direct contradiction, not
|
|
188
|
+
* mere lag. This is the issue's canonical example: complete in STATE.md but
|
|
189
|
+
* in progress in ROADMAP.md.
|
|
190
|
+
*
|
|
191
|
+
* 'Deferred' is a second declared-intent rank-0 status (gsd-core/templates/
|
|
192
|
+
* roadmap.md:133's full vocabulary: `Not started | In progress | Complete |
|
|
193
|
+
* Deferred`) that is NOT ordinary lag from rank 0: it is an explicit
|
|
194
|
+
* decision to STOP work, not merely "hasn't started yet". If exactly one
|
|
195
|
+
* side declares 'deferred' and the other side's rank is >= 1 (work is
|
|
196
|
+
* reported as in progress or complete), that is always 'drifted' — a phase
|
|
197
|
+
* declared deferred while the other document says work is happening is a
|
|
198
|
+
* direct contradiction, checked here (like the terminal-completeness rule
|
|
199
|
+
* above) BEFORE the numeric distance comparison. 'Deferred' against a
|
|
200
|
+
* rank-0 status on the other side (e.g. 'Not started') stays 'consistent' —
|
|
201
|
+
* both agree no work has happened.
|
|
202
|
+
*
|
|
203
|
+
* Otherwise, ranks are compared numerically: equal → 'consistent';
|
|
204
|
+
* off-by-one → 'lag'; off-by-two-or-more → 'drifted'.
|
|
205
|
+
*
|
|
206
|
+
* @param opts.stateStatus - raw Status value from STATE.md's Current Position
|
|
207
|
+
* @param opts.roadmapStatus - raw Status cell from ROADMAP.md's Progress table
|
|
208
|
+
* @returns { verdict, stateRank, roadmapRank }
|
|
209
|
+
*/
|
|
210
|
+
function comparePhaseStatus({ stateStatus, roadmapStatus, }) {
|
|
211
|
+
const stateRank = resolvePhaseStatusRank(stateStatus);
|
|
212
|
+
const roadmapRank = resolvePhaseStatusRank(roadmapStatus);
|
|
213
|
+
if (stateRank === null || roadmapRank === null) {
|
|
214
|
+
return { verdict: 'uncheckable', stateRank, roadmapRank };
|
|
215
|
+
}
|
|
216
|
+
const stateIsTerminal = stateRank === TERMINAL_RANK;
|
|
217
|
+
const roadmapIsTerminal = roadmapRank === TERMINAL_RANK;
|
|
218
|
+
if (stateIsTerminal !== roadmapIsTerminal) {
|
|
219
|
+
return { verdict: 'drifted', stateRank, roadmapRank };
|
|
220
|
+
}
|
|
221
|
+
const stateIsDeferred = normalizePhaseStatusText(stateStatus) === 'deferred';
|
|
222
|
+
const roadmapIsDeferred = normalizePhaseStatusText(roadmapStatus) === 'deferred';
|
|
223
|
+
if (stateIsDeferred !== roadmapIsDeferred) {
|
|
224
|
+
const otherRank = stateIsDeferred ? roadmapRank : stateRank;
|
|
225
|
+
if (otherRank >= 1) {
|
|
226
|
+
return { verdict: 'drifted', stateRank, roadmapRank };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const diff = Math.abs(stateRank - roadmapRank);
|
|
230
|
+
if (diff === 0) {
|
|
231
|
+
return { verdict: 'consistent', stateRank, roadmapRank };
|
|
232
|
+
}
|
|
233
|
+
if (diff === 1) {
|
|
234
|
+
return { verdict: 'lag', stateRank, roadmapRank };
|
|
235
|
+
}
|
|
236
|
+
return { verdict: 'drifted', stateRank, roadmapRank };
|
|
237
|
+
}
|
|
@@ -15,6 +15,12 @@ const { countMatchedSummaries } = coreUtils;
|
|
|
15
15
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
16
16
|
const frontmatterMod = require("./frontmatter.cjs");
|
|
17
17
|
const { extractFrontmatter } = frontmatterMod;
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
19
|
+
const planningScopeMod = require("./planning-scope.cjs");
|
|
20
|
+
const { SCOPE } = planningScopeMod;
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
|
+
const planDependencyGraphMod = require("./plan-dependency-graph.cjs");
|
|
23
|
+
const { isSummaryFileBlocked } = planDependencyGraphMod;
|
|
18
24
|
// Excluded derivative files
|
|
19
25
|
const PLAN_OUTLINE_RE = /-OUTLINE\.md$/i;
|
|
20
26
|
const PLAN_PRE_BOUNCE_RE = /\.pre-bounce\.md$/i;
|
|
@@ -97,6 +103,43 @@ function isRootSummaryFile(fileName) {
|
|
|
97
103
|
function isNestedSummaryFile(fileName) {
|
|
98
104
|
return /^SUMMARY-\d+.*\.md$/i.test(fileName) || /-SUMMARY-\d+.*\.md$/i.test(fileName);
|
|
99
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Strict canonical-naming predicate over a `scanPhasePlans` `planFiles`/
|
|
108
|
+
* `allPlanFiles` ENTRY (root form bare, nested form `plans/`-prefixed, exactly
|
|
109
|
+
* as those arrays store them) — root `<phase>-<NN>-PLAN.md`/bare `PLAN.md`,
|
|
110
|
+
* or nested `plans/PLAN-<NN>....md`/`plans/<x>-PLAN-<NN>....md` — WITHOUT
|
|
111
|
+
* `isRootPlanFile`'s loose `/\.md$/i && /PLAN/i` fallback.
|
|
112
|
+
*
|
|
113
|
+
* The `plans/` prefix check is load-bearing, not cosmetic: `isNestedPlanFile`
|
|
114
|
+
* matches ANY basename containing `-PLAN-<digits>...md` with no anchor
|
|
115
|
+
* requiring an actual `plans/` directory — that shape is exactly the #2893
|
|
116
|
+
* reporter's non-canonical example, `01-PLAN-01-foundation.md`. Applying
|
|
117
|
+
* `isNestedPlanFile` directly to a bare root-level name would therefore
|
|
118
|
+
* misclassify that exact offender as canonical. Only entries scanPhasePlans
|
|
119
|
+
* itself produced with the `plans/` prefix (i.e. read from the real nested
|
|
120
|
+
* subdirectory) are eligible for the nested check.
|
|
121
|
+
*
|
|
122
|
+
* #2893/#3183: `isRootPlanFile`'s loose fallback is deliberately permissive
|
|
123
|
+
* for live-plan COUNTING (a lowercase `plan.md` still counts toward
|
|
124
|
+
* completion — see plan-count-single-owner.test.cjs's pinned case-sensitivity
|
|
125
|
+
* asymmetry). But the #2893 "non-canonical filename" diagnostic (phase.cts's
|
|
126
|
+
* `describeNonCanonicalPlans`, used by find-phase/phase-plan-index/phases
|
|
127
|
+
* list --type plans) exists specifically to CATCH a plan-shaped file that
|
|
128
|
+
* does NOT match the canonical contract and warn instead of silently
|
|
129
|
+
* scheduling it. Feeding that diagnostic (and the `plans`/`files` lists those
|
|
130
|
+
* commands return) the loose `allPlanFiles`/`planFiles` set defeats the
|
|
131
|
+
* diagnostic entirely, since the loose fallback already recognizes the
|
|
132
|
+
* non-canonical file as "matched". This predicate is the STRICT filter those
|
|
133
|
+
* three call sites intersect against so the diagnostic (and what counts as a
|
|
134
|
+
* schedulable plan for those commands specifically) stays canonical-only,
|
|
135
|
+
* while scanPhasePlans's own planCount/summaryCount/completed stay on the
|
|
136
|
+
* loose, permissive rule.
|
|
137
|
+
*/
|
|
138
|
+
function isCanonicalPlanFile(fileEntry) {
|
|
139
|
+
if (fileEntry.startsWith('plans/'))
|
|
140
|
+
return isNestedPlanFile(fileEntry.slice('plans/'.length));
|
|
141
|
+
return fileEntry.endsWith('-PLAN.md') || fileEntry === 'PLAN.md';
|
|
142
|
+
}
|
|
100
143
|
function scanPhasePlans(phaseDir) {
|
|
101
144
|
let rootFiles;
|
|
102
145
|
try {
|
|
@@ -109,7 +152,9 @@ function scanPhasePlans(phaseDir) {
|
|
|
109
152
|
completed: false,
|
|
110
153
|
hasNestedPlans: false,
|
|
111
154
|
planFiles: [],
|
|
155
|
+
allPlanFiles: [],
|
|
112
156
|
summaryFiles: [],
|
|
157
|
+
scope: SCOPE.UNREADABLE,
|
|
113
158
|
};
|
|
114
159
|
}
|
|
115
160
|
const rootPlanFiles = rootFiles.filter(isRootPlanFile);
|
|
@@ -117,6 +162,7 @@ function scanPhasePlans(phaseDir) {
|
|
|
117
162
|
let nestedPlanFiles = [];
|
|
118
163
|
let nestedSummaryFiles = [];
|
|
119
164
|
let hasNestedPlans = false;
|
|
165
|
+
let scope = SCOPE.COMPLETE;
|
|
120
166
|
const nestedDir = (0, node_path_1.join)(phaseDir, 'plans');
|
|
121
167
|
if ((0, node_fs_1.existsSync)(nestedDir)) {
|
|
122
168
|
try {
|
|
@@ -125,7 +171,12 @@ function scanPhasePlans(phaseDir) {
|
|
|
125
171
|
nestedSummaryFiles = nestedFiles.filter(isNestedSummaryFile).map((file) => `plans/${file}`);
|
|
126
172
|
hasNestedPlans = nestedPlanFiles.length > 0;
|
|
127
173
|
}
|
|
128
|
-
catch {
|
|
174
|
+
catch {
|
|
175
|
+
// #3183 (ADR-3180 Decision 2): the nested plans/ dir exists but could not
|
|
176
|
+
// be read — this scan cannot see plans it knows are there, so zero is
|
|
177
|
+
// NOT a reliable answer; mark TRUNCATED rather than COMPLETE.
|
|
178
|
+
scope = SCOPE.TRUNCATED;
|
|
179
|
+
}
|
|
129
180
|
}
|
|
130
181
|
const allPlanFiles = rootPlanFiles.concat(nestedPlanFiles);
|
|
131
182
|
// #2349: drop plans explicitly marked `status: superseded` from the plan set
|
|
@@ -144,7 +195,18 @@ function scanPhasePlans(phaseDir) {
|
|
|
144
195
|
// 30-GAPCLOSURE-SUMMARY.md) must not inflate summary_count or flip a phase to
|
|
145
196
|
// Complete when plans are still missing summaries. summaryFiles (the array)
|
|
146
197
|
// still holds every summary on disk for callers that read/list them.
|
|
147
|
-
|
|
198
|
+
//
|
|
199
|
+
// #3345: a SUMMARY whose frontmatter declares `status: blocked` is a failure
|
|
200
|
+
// record, not a completion record — it is dropped from the COUNTABLE pairing
|
|
201
|
+
// set before matching. The bounded-prefix status read is the SHARED predicate
|
|
202
|
+
// (plan-dependency-graph.cjs's isSummaryFileBlocked) that phase.cts's read
|
|
203
|
+
// path also filters through, so the count and the `incomplete` list cannot
|
|
204
|
+
// diverge. Fail-open: a SUMMARY with no `status` key, or one that cannot be read,
|
|
205
|
+
// keeps its pre-#3345 filename-existence meaning — untouched projects are
|
|
206
|
+
// byte-for-behaviour identical. `status: halted` stays counted (#2830: a
|
|
207
|
+
// designed stop still writes a completion record).
|
|
208
|
+
const countableSummaryFiles = summaryFiles.filter((f) => !isSummaryFileBlocked((0, node_path_1.join)(phaseDir, f)));
|
|
209
|
+
const summaryCount = countMatchedSummaries(planFiles, countableSummaryFiles);
|
|
148
210
|
return {
|
|
149
211
|
planCount,
|
|
150
212
|
summaryCount,
|
|
@@ -155,10 +217,31 @@ function scanPhasePlans(phaseDir) {
|
|
|
155
217
|
// (0 >= 0) rather than being pinned below 100% forever, which is the very
|
|
156
218
|
// failure this fix removes. A genuinely empty phase (no plans authored)
|
|
157
219
|
// still has allPlanFiles.length 0 and stays not-completed, exactly as before.
|
|
220
|
+
//
|
|
221
|
+
// ADR-3180 §7.4 (issue #3186) — DELIBERATELY NOT routed through
|
|
222
|
+
// `isPhaseComplete` (src/verification.cts). This field answers "are all
|
|
223
|
+
// plans summarized?", NOT "is the phase complete?" — completion
|
|
224
|
+
// additionally requires a passing `*-VERIFICATION.md`, which is the
|
|
225
|
+
// whole point of that owner's unconditional readVerificationStatus call.
|
|
226
|
+
// Folding this field onto `isPhaseComplete` would either over-report
|
|
227
|
+
// completion (a phase whose plans are done but never verified) or drag a
|
|
228
|
+
// verification read into this module, inverting the dependency
|
|
229
|
+
// direction between this Phase-1 owner (plan counting) and the Phase-4
|
|
230
|
+
// owner (completion) — the owner must consume plan counts, never the
|
|
231
|
+
// reverse. Kept as its own, differently-scoped answer per the design's
|
|
232
|
+
// "0.x split" and exempted (function-scoped, not file-scoped) in
|
|
233
|
+
// scripts/lint-completion-predicate-drift.cjs's FUNCTION_SCOPED_EXEMPTIONS.
|
|
234
|
+
// The field name is left unchanged (not renamed to e.g.
|
|
235
|
+
// `summariesMeetPlanCount`) — scanPhasePlans has 11 direct callers, and a
|
|
236
|
+
// rename's blast radius is out of this phase's declared scope; noted
|
|
237
|
+
// here as a deliberate, considered-and-declined option rather than an
|
|
238
|
+
// oversight.
|
|
158
239
|
completed: allPlanFiles.length > 0 && summaryCount >= planCount,
|
|
159
240
|
hasNestedPlans,
|
|
160
241
|
planFiles,
|
|
242
|
+
allPlanFiles,
|
|
161
243
|
summaryFiles,
|
|
244
|
+
scope,
|
|
162
245
|
};
|
|
163
246
|
}
|
|
164
247
|
module.exports = Object.assign(scanPhasePlans, {
|
|
@@ -167,4 +250,5 @@ module.exports = Object.assign(scanPhasePlans, {
|
|
|
167
250
|
isNestedPlanFile,
|
|
168
251
|
isRootSummaryFile,
|
|
169
252
|
isNestedSummaryFile,
|
|
253
|
+
isCanonicalPlanFile,
|
|
170
254
|
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Planning Scope — shared result discriminator for live-plan scanning.
|
|
4
|
+
*
|
|
5
|
+
* ADR-3180 Decision 2 (docs/adr/3180-planning-semantic-model-single-owner.md):
|
|
6
|
+
* `scanPhasePlans` is the single owner of live-plan counting, and every
|
|
7
|
+
* consumer of that count needs to distinguish a REAL answer from a
|
|
8
|
+
* NON-answer. `SCOPE.COMPLETE` with zero items is a real answer — a phase
|
|
9
|
+
* genuinely has no plans yet. `SCOPE.TRUNCATED`, `SCOPE.UNSCOPED`, and
|
|
10
|
+
* `SCOPE.UNREADABLE` with zero items are NOT — they mean the scan could not
|
|
11
|
+
* see (part of) the phase directory, so a caller must not treat that zero as
|
|
12
|
+
* "this phase has no plans."
|
|
13
|
+
*
|
|
14
|
+
* This is a frozen enum, not a message string: CONTRIBUTING.md bans raw-text
|
|
15
|
+
* matching on outputs and requires a typed IR, so callers branch on the
|
|
16
|
+
* `SCOPE` value rather than pattern-matching prose.
|
|
17
|
+
*
|
|
18
|
+
* PROVISIONAL: this contract is pending this phase's validation and may be
|
|
19
|
+
* revised before the epic (#3180) ships.
|
|
20
|
+
*
|
|
21
|
+
* Dependencies: none — this is a leaf module (mirrors src/phase-id.cts). It
|
|
22
|
+
* imports nothing, so any consumer can depend on it without risking a cycle.
|
|
23
|
+
*/
|
|
24
|
+
const SCOPE = Object.freeze({
|
|
25
|
+
COMPLETE: 'complete',
|
|
26
|
+
TRUNCATED: 'truncated',
|
|
27
|
+
UNSCOPED: 'unscoped',
|
|
28
|
+
UNREADABLE: 'unreadable',
|
|
29
|
+
});
|
|
30
|
+
const planningScope = { SCOPE };
|
|
31
|
+
module.exports = planningScope;
|