@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
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.STATE_MD_SECTIONS = exports.FIELD_CLASSIFICATION = void 0;
|
|
19
19
|
exports.getFieldClassification = getFieldClassification;
|
|
20
|
+
exports.applyPreserveWhenUnchanged = applyPreserveWhenUnchanged;
|
|
20
21
|
exports.applyStatePreservation = applyStatePreservation;
|
|
21
22
|
exports.transitionCore = transitionCore;
|
|
22
23
|
exports.sliceCurrentPositionSection = sliceCurrentPositionSection;
|
|
@@ -26,10 +27,8 @@ const state_document_cjs_1 = require("./state-document.cjs");
|
|
|
26
27
|
const state_document_cjs_2 = require("./state-document.cjs");
|
|
27
28
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
28
29
|
const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
|
|
29
|
-
|
|
30
|
-
const phaseIdMod = require("./phase-id.cjs");
|
|
30
|
+
const pattern_cjs_1 = require("./pattern.cjs");
|
|
31
31
|
const { extractFrontmatter, reconstructFrontmatter, stripFrontmatter } = frontmatter;
|
|
32
|
-
const { escapeRegex } = phaseIdMod;
|
|
33
32
|
// Stop predicate for section-body slicing: a level-2+ heading ends the section.
|
|
34
33
|
const STOP_H2_PLUS = (lv) => lv >= 2;
|
|
35
34
|
/**
|
|
@@ -54,18 +53,31 @@ exports.FIELD_CLASSIFICATION = Object.freeze(Object.assign(Object.create(null),
|
|
|
54
53
|
milestone_name: { source: 'external', preservation: 'preserve-if-placeholder' },
|
|
55
54
|
// Phase / plan position (body-derived)
|
|
56
55
|
current_phase: { source: 'body', preservation: 'preserve-when-unchanged' },
|
|
57
|
-
|
|
56
|
+
// #1743, #1695. #3468: row corrected to match its long-standing behavior
|
|
57
|
+
// — was declared preserve-always, has always been delta-gated (only
|
|
58
|
+
// restores when the body `Phase:` source is unchanged this write).
|
|
59
|
+
current_phase_name: { source: 'curated', preservation: 'preserve-when-unchanged' },
|
|
58
60
|
current_plan: { source: 'body', preservation: 'preserve-when-unchanged' },
|
|
59
61
|
// Status / lifecycle (body-derived; #1230 delta heuristic applies)
|
|
60
|
-
|
|
62
|
+
// guard: the 'unknown' sentinel is the ONLY true executor-side guard in
|
|
63
|
+
// this table (stopped_at's `## Session` scoping is caller-side delta
|
|
64
|
+
// extraction, not an executor condition) — ADR-3408 Decision 1.
|
|
65
|
+
status: { source: 'body', preservation: 'preserve-when-unchanged', guard: 'non-sentinel-unknown' },
|
|
61
66
|
stopped_at: { source: 'body', preservation: 'preserve-when-unchanged' },
|
|
62
67
|
paused_at: { source: 'body', preservation: 'preserve-when-unchanged' },
|
|
63
68
|
// Activity log
|
|
64
69
|
last_updated: { source: 'free', preservation: 'derive' }, // realClock.nowIso()
|
|
65
70
|
last_activity: { source: 'body', preservation: 'derive' }, // always refresh on transition
|
|
66
71
|
last_activity_desc: { source: 'body', preservation: 'preserve-when-unchanged' },
|
|
72
|
+
// Commit provenance (#2573) — ambient git read, recomputed on every write,
|
|
73
|
+
// exactly like last_updated. Never preserved: a stale stamp would claim
|
|
74
|
+
// STATE.md was written against a commit it wasn't.
|
|
75
|
+
state_head: { source: 'free', preservation: 'derive' }, // #2573
|
|
67
76
|
// Progress block (disk-derived, except the curated progress ratchet)
|
|
68
|
-
|
|
77
|
+
// mergeStrategy: 'progress-ratchet' — completed_plans/completed_phases
|
|
78
|
+
// only ever ratchet UP toward the derived value (#2969); everything
|
|
79
|
+
// else in the merge is either always-derived (#2440) or always-curated.
|
|
80
|
+
progress: { source: 'curated', preservation: 'preserve-always', mergeStrategy: 'progress-ratchet' }, // #3242, #1446
|
|
69
81
|
'progress.total_phases': { source: 'disk', preservation: 'derive' },
|
|
70
82
|
'progress.completed_phases': { source: 'disk', preservation: 'derive' },
|
|
71
83
|
'progress.total_plans': { source: 'disk', preservation: 'derive' },
|
|
@@ -82,83 +94,216 @@ function getFieldClassification(field) {
|
|
|
82
94
|
return exports.FIELD_CLASSIFICATION[field];
|
|
83
95
|
}
|
|
84
96
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
97
|
+
* ADR-3408 §8.2: an unenforced `preserve-when-unchanged` row throws. Both
|
|
98
|
+
* ends of this invariant are gsd-core's own source — a declared row the
|
|
99
|
+
* *caller code* forgot to wire via `bodyDeltas` — so it is a programming
|
|
100
|
+
* error, unreachable from any user document. The bright line, stated because
|
|
101
|
+
* conflating its two sides would be severe: a drifted, malformed, or
|
|
102
|
+
* unparseable user STATE.md NEVER reaches this throw (§8.5 governs that case
|
|
103
|
+
* with preserve-and-warn); this fires only when the *caller* omitted a
|
|
104
|
+
* `bodyDeltas` entry for a row the table itself declares. Getting this
|
|
105
|
+
* backwards turns every desynced project's `phase.complete` into a hard
|
|
106
|
+
* failure.
|
|
88
107
|
*/
|
|
89
|
-
function
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
108
|
+
function throwUnwiredRow(field) {
|
|
109
|
+
const err = new Error(`applyStatePreservation: preserve-when-unchanged row ${JSON.stringify(field)} reached the ` +
|
|
110
|
+
'executor with no wired ctx.bodyDeltas entry. This is an internal invariant violation (ADR-3408 ' +
|
|
111
|
+
'§8.2) — the caller (readModifyWriteStateMd) forgot to supply this field\'s body-source delta. ' +
|
|
112
|
+
'Add a bodyDeltas entry for this field per ADR-3408 §8.3, or remove the row from ' +
|
|
113
|
+
'FIELD_CLASSIFICATION if the field no longer needs this policy.');
|
|
114
|
+
err.code = 'STATE_PRESERVATION_UNWIRED_ROW';
|
|
115
|
+
err.field = field;
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Executor for `preservation: 'preserve-when-unchanged'` (ADR-3408 §8.1). The
|
|
120
|
+
* #1230 delta heuristic: restore the pre-write frontmatter snapshot when this
|
|
121
|
+
* write did not change the field's body source, and the snapshot is a real
|
|
122
|
+
* (non-empty-after-trim) curated value the derived value should not clobber.
|
|
123
|
+
*
|
|
124
|
+
* Every row carrying this policy — status, stopped_at, current_phase_name,
|
|
125
|
+
* current_phase, current_plan, paused_at, last_activity_desc — is honored by
|
|
126
|
+
* this ONE executor; `cls.guard` is the only field-specific variation (the
|
|
127
|
+
* closed vocabulary of ADR-3408 Decision 1).
|
|
128
|
+
*
|
|
129
|
+
* Exported (ADR-3408 §8.5 / D3) so `cmdStateJson` (state.cts) — a read-only
|
|
130
|
+
* path with no transform of its own — can route its stale-vs-fresh decision
|
|
131
|
+
* through the SAME executor the write path uses, rather than maintaining a
|
|
132
|
+
* third private copy of this policy. `cmdStateJson` calls this directly
|
|
133
|
+
* (not the full `applyStatePreservation` dispatch loop) so its read stays
|
|
134
|
+
* scoped to exactly the fields it has always governed and never touches
|
|
135
|
+
* `progress` or `milestone*`, which are different policies with their own
|
|
136
|
+
* read-path rules (`shouldPreserveExistingProgress`, `preserve-if-placeholder`).
|
|
137
|
+
*/
|
|
138
|
+
function applyPreserveWhenUnchanged(field, cls, ctx) {
|
|
139
|
+
// 1. A declared row with no wired delta is an internal invariant violation
|
|
140
|
+
// — throw (ADR-3408 §8.2). Never reached for a user-document defect: the
|
|
141
|
+
// production caller (readModifyWriteStateMd) wires every preserve-when-
|
|
142
|
+
// unchanged row unconditionally.
|
|
143
|
+
const delta = ctx.bodyDeltas ? ctx.bodyDeltas[field] : undefined;
|
|
144
|
+
if (!delta)
|
|
145
|
+
throwUnwiredRow(field);
|
|
146
|
+
// 2. Only a real, non-whitespace-only curated string is worth restoring
|
|
147
|
+
// (#3468: tightened from `.length > 0` to a trimmed check — a whitespace-
|
|
148
|
+
// only snapshot is not a real curated value).
|
|
149
|
+
const snapshot = ctx.preFmSnapshot[field];
|
|
150
|
+
if (typeof snapshot !== 'string' || snapshot.trim().length === 0)
|
|
151
|
+
return;
|
|
152
|
+
// 3. Closed-vocabulary guard: status's 'unknown' sentinel is never restored.
|
|
153
|
+
// Exact-match, case-sensitive — 'Unknown' is a real value and IS restored.
|
|
154
|
+
if (cls.guard === 'non-sentinel-unknown' && snapshot === 'unknown')
|
|
155
|
+
return;
|
|
156
|
+
// 4. The body source changed this write → the freshly-derived value wins.
|
|
157
|
+
if (delta.pre !== delta.post)
|
|
158
|
+
return;
|
|
159
|
+
// 5. Already correct → no-op (avoid a spurious `mutated=true`).
|
|
160
|
+
if (ctx.postFm[field] === snapshot)
|
|
161
|
+
return;
|
|
162
|
+
// 6. Restore.
|
|
163
|
+
ctx.postFm[field] = snapshot;
|
|
164
|
+
ctx.mutated = true;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Executor for `preservation: 'preserve-always'` (ADR-3408 §8.1). Only
|
|
168
|
+
* `progress` carries this policy today. Preserves #3242/#1446/#2440/#2969
|
|
169
|
+
* semantics byte-for-byte: gated on `!resync` and a truthy `preFm[field]`;
|
|
170
|
+
* the `mergeStrategy: 'progress-ratchet'` per-key merge only fires when the
|
|
171
|
+
* caller opts in via `deriveProgressKeys`, else the whole curated block wins
|
|
172
|
+
* wholesale.
|
|
173
|
+
*/
|
|
174
|
+
function applyPreserveAlways(field, cls, ctx) {
|
|
175
|
+
if (ctx.resync || !ctx.preFm || !ctx.preFm[field])
|
|
176
|
+
return;
|
|
177
|
+
if (cls.mergeStrategy === 'progress-ratchet' && ctx.deriveProgressKeys && ctx.postFm[field]) {
|
|
178
|
+
// #2440: total_plans and total_phases always take the derived (post-sync)
|
|
179
|
+
// value even under !resync. This is used by cmdStatePlannedPhase where
|
|
180
|
+
// total_plans must correct upward after plans are added. For body-only
|
|
181
|
+
// writes (state.update/patch without the flag), the wholesale restore
|
|
182
|
+
// below preserves everything as before — the #3242 Bug A protection
|
|
183
|
+
// stays fully in force.
|
|
184
|
+
const curated = ctx.preFm[field];
|
|
185
|
+
const derived = (ctx.postFm[field] ?? {});
|
|
186
|
+
const merged = { ...derived };
|
|
187
|
+
if (curated) {
|
|
188
|
+
// #2440: total_plans and total_phases always take the derived value.
|
|
189
|
+
// #2969: completed_plans and completed_phases take the derived value
|
|
190
|
+
// when it is GREATER than the curated value (gap-closure plans that
|
|
191
|
+
// completed after the plan count grew) — ratcheting UP only, never
|
|
192
|
+
// deriving downward (preserves the #3242 curated-progress protection
|
|
193
|
+
// for cases unrelated to plan-count growth, e.g. a deleted SUMMARY).
|
|
194
|
+
// percent also takes the derived value — the resync recomputed it from
|
|
195
|
+
// disk counts, and a stale curated percent would be incoherent against
|
|
196
|
+
// the ratcheted-up completed counts (e.g. 54/54 at 93%).
|
|
197
|
+
const ratchetUpKeys = new Set(['completed_plans', 'completed_phases']);
|
|
198
|
+
for (const [key, value] of Object.entries(curated)) {
|
|
199
|
+
if (key === 'total_plans' || key === 'total_phases' || key === 'percent')
|
|
200
|
+
continue;
|
|
201
|
+
if (ratchetUpKeys.has(key)) {
|
|
202
|
+
const derivedNum = typeof derived[key] === 'number' ? derived[key] : -Infinity;
|
|
203
|
+
const curatedNum = typeof value === 'number' ? value : -Infinity;
|
|
204
|
+
// Take the derived value only when it ratchets up (strictly
|
|
205
|
+
// greater — #2969's `>` not `>=`); else keep curated.
|
|
206
|
+
if (derivedNum > curatedNum)
|
|
207
|
+
continue;
|
|
208
|
+
merged[key] = value;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
merged[key] = value;
|
|
118
212
|
}
|
|
119
213
|
}
|
|
120
|
-
postFm['progress'] = merged;
|
|
121
214
|
}
|
|
122
|
-
|
|
123
|
-
|
|
215
|
+
ctx.postFm[field] = merged;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
ctx.postFm[field] = ctx.preFm[field];
|
|
219
|
+
}
|
|
220
|
+
ctx.mutated = true;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Executor for `preservation: 'preserve-if-placeholder'` (ADR-3408 §8.1).
|
|
224
|
+
* `milestone` and `milestone_name` both carry this policy in the table, and
|
|
225
|
+
* both rows dispatch into this SAME executor body — no branch is selected by
|
|
226
|
+
* field name (ADR-3408 §8.1). The body always restores the name+version pair
|
|
227
|
+
* together (#948/#2135), ignoring which of the two rows triggered the call;
|
|
228
|
+
* this is deliberately safe because the executor is idempotent: whichever
|
|
229
|
+
* row fires first either performs the restore (after which the second row's
|
|
230
|
+
* call recomputes against already-restored state and finds nothing left to
|
|
231
|
+
* do) or finds no placeholder to restore (in which case the second row's
|
|
232
|
+
* call, seeing the same unchanged inputs, reaches the same conclusion). Two
|
|
233
|
+
* dispatches per write converge to the identical single-pass result, so the
|
|
234
|
+
* field argument itself is unused here — it exists only to satisfy the
|
|
235
|
+
* shared executor signature every policy branch in the dispatch loop shares.
|
|
236
|
+
*/
|
|
237
|
+
function applyPreserveIfPlaceholder(_field, _cls, ctx) {
|
|
238
|
+
const MILESTONE_PLACEHOLDER = 'milestone';
|
|
239
|
+
const derivedName = ctx.postFm['milestone_name'];
|
|
240
|
+
const derivedLooksLikeName = typeof derivedName === 'string'
|
|
241
|
+
&& derivedName.length > 0
|
|
242
|
+
&& derivedName !== MILESTONE_PLACEHOLDER
|
|
243
|
+
&& !/^[\s—–:-]/.test(derivedName);
|
|
244
|
+
const snapshotName = ctx.preFmSnapshot['milestone_name'];
|
|
245
|
+
const snapshotNameIsReal = typeof snapshotName === 'string'
|
|
246
|
+
&& snapshotName.length > 0
|
|
247
|
+
&& snapshotName !== MILESTONE_PLACEHOLDER;
|
|
248
|
+
if (derivedLooksLikeName || !snapshotNameIsReal)
|
|
249
|
+
return;
|
|
250
|
+
if (ctx.postFm['milestone_name'] !== snapshotName) {
|
|
251
|
+
ctx.postFm['milestone_name'] = snapshotName;
|
|
252
|
+
ctx.mutated = true;
|
|
253
|
+
}
|
|
254
|
+
const snapshotVersion = ctx.preFmSnapshot['milestone'];
|
|
255
|
+
if (typeof snapshotVersion === 'string' && snapshotVersion.length > 0 &&
|
|
256
|
+
ctx.postFm['milestone'] !== snapshotVersion) {
|
|
257
|
+
ctx.postFm['milestone'] = snapshotVersion;
|
|
258
|
+
ctx.mutated = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Executor for `preservation: 'derive'`. Explicit no-op — the sync's
|
|
263
|
+
* freshly-derived value stands untouched. Naming this executor (rather than
|
|
264
|
+
* skipping `derive` rows by omission) is what makes ADR-3408 §8.2's throw
|
|
265
|
+
* decidable: "policy says do nothing" is now distinguishable from "nobody
|
|
266
|
+
* wired this", because every member of `FieldPreservation` reaches an
|
|
267
|
+
* executor.
|
|
268
|
+
*/
|
|
269
|
+
function applyDerive(_field, _cls, _ctx) {
|
|
270
|
+
// No-op by design — see docstring.
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Pure, table-driven post-sync preservation (ADR-3408 §8.1). One loop over
|
|
274
|
+
* `FIELD_CLASSIFICATION`, dispatching on the row's `preservation` value —
|
|
275
|
+
* never on the field name. Mutates `postFm` in place to mirror the
|
|
276
|
+
* pre-#3468 inline block (which also mutated in place) and returns whether
|
|
277
|
+
* any field was restored.
|
|
278
|
+
*/
|
|
279
|
+
function applyStatePreservation(input) {
|
|
280
|
+
const ctx = {
|
|
281
|
+
preFm: input.preFm,
|
|
282
|
+
postFm: input.postFm,
|
|
283
|
+
preFmSnapshot: input.preFmSnapshot,
|
|
284
|
+
resync: input.resync,
|
|
285
|
+
deriveProgressKeys: input.deriveProgressKeys === true,
|
|
286
|
+
bodyDeltas: input.bodyDeltas,
|
|
287
|
+
mutated: false,
|
|
288
|
+
};
|
|
289
|
+
for (const field of Object.keys(exports.FIELD_CLASSIFICATION)) {
|
|
290
|
+
const cls = getFieldClassification(field);
|
|
291
|
+
if (!cls)
|
|
292
|
+
continue;
|
|
293
|
+
if (cls.preservation === 'preserve-when-unchanged') {
|
|
294
|
+
applyPreserveWhenUnchanged(field, cls, ctx);
|
|
124
295
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
postFm['status'] = preFmSnapshot['status'];
|
|
137
|
-
mutated = true;
|
|
138
|
-
}
|
|
139
|
-
// stopped_at — same #1230 body-delta heuristic. Table: preserve-when-unchanged.
|
|
140
|
-
const stoppedCls = getFieldClassification('stopped_at');
|
|
141
|
-
if (stoppedCls !== null &&
|
|
142
|
-
stoppedCls.preservation === 'preserve-when-unchanged' &&
|
|
143
|
-
input.postBodyStoppedAt === input.preBodyStoppedAt &&
|
|
144
|
-
typeof preFmSnapshot['stopped_at'] === 'string' &&
|
|
145
|
-
preFmSnapshot['stopped_at'].length > 0 &&
|
|
146
|
-
postFm['stopped_at'] !== preFmSnapshot['stopped_at']) {
|
|
147
|
-
postFm['stopped_at'] = preFmSnapshot['stopped_at'];
|
|
148
|
-
mutated = true;
|
|
149
|
-
}
|
|
150
|
-
// current_phase_name — curated (#1743/#1695). Table: preserve-always.
|
|
151
|
-
const phaseNameCls = getFieldClassification('current_phase_name');
|
|
152
|
-
if (phaseNameCls !== null &&
|
|
153
|
-
phaseNameCls.preservation === 'preserve-always' &&
|
|
154
|
-
input.postBodyPhaseSource === input.preBodyPhaseSource &&
|
|
155
|
-
typeof preFmSnapshot['current_phase_name'] === 'string' &&
|
|
156
|
-
preFmSnapshot['current_phase_name'].length > 0 &&
|
|
157
|
-
postFm['current_phase_name'] !== preFmSnapshot['current_phase_name']) {
|
|
158
|
-
postFm['current_phase_name'] = preFmSnapshot['current_phase_name'];
|
|
159
|
-
mutated = true;
|
|
160
|
-
}
|
|
161
|
-
return { postFm, mutated };
|
|
296
|
+
else if (cls.preservation === 'preserve-always') {
|
|
297
|
+
applyPreserveAlways(field, cls, ctx);
|
|
298
|
+
}
|
|
299
|
+
else if (cls.preservation === 'preserve-if-placeholder') {
|
|
300
|
+
applyPreserveIfPlaceholder(field, cls, ctx);
|
|
301
|
+
}
|
|
302
|
+
else if (cls.preservation === 'derive') {
|
|
303
|
+
applyDerive(field, cls, ctx);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return { postFm: ctx.postFm, mutated: ctx.mutated };
|
|
162
307
|
}
|
|
163
308
|
// ----------------------------------------------------------------------------
|
|
164
309
|
// Body section constants (ADR-1769 §6 — single writer after migration)
|
|
@@ -279,7 +424,7 @@ function beginPhaseCore(content, intent, deps) {
|
|
|
279
424
|
// Extract from body (not full content) so the YAML `status:` key cannot
|
|
280
425
|
// shadow the body Status field (#1255).
|
|
281
426
|
const currentStatus = (0, state_document_cjs_1.stateExtractField)(body, 'Status') || '';
|
|
282
|
-
const isAlreadyExecuting = new RegExp(`Executing Phase\\s+${escapeRegex(String(intent.phaseNumber))}\\b`, 'i').test(currentStatus);
|
|
427
|
+
const isAlreadyExecuting = new RegExp(`Executing Phase\\s+${(0, pattern_cjs_1.escapeRegex)(String(intent.phaseNumber))}\\b`, 'i').test(currentStatus);
|
|
283
428
|
// Status update (applies on both first-time and resume — Status is always refreshed).
|
|
284
429
|
tryField('Status', `Executing Phase ${intent.phaseNumber}`);
|
|
285
430
|
// Last Activity date — safe to refresh on resume (tracks when execute-phase ran).
|
|
@@ -339,7 +484,24 @@ function locateCurrentPosition(body) {
|
|
|
339
484
|
let end = body.length;
|
|
340
485
|
for (let j = idx + 1; j < hs.length; j++) {
|
|
341
486
|
if (STOP_H2_PLUS(hs[j].level)) {
|
|
342
|
-
|
|
487
|
+
// Exclude the newline that separates this section from the next
|
|
488
|
+
// heading. Walk back over a bare `\n`, then over a `\r` if one
|
|
489
|
+
// immediately precedes it (CRLF), so a CRLF document's slice does not
|
|
490
|
+
// retain a stray unpaired trailing `\r` (#3118).
|
|
491
|
+
let e = hs[j].offset;
|
|
492
|
+
if (e > 0 && body[e - 1] === '\n') {
|
|
493
|
+
e -= 1;
|
|
494
|
+
if (e > 0 && body[e - 1] === '\r')
|
|
495
|
+
e -= 1;
|
|
496
|
+
}
|
|
497
|
+
// Clamp so the span can never invert (#3118 review): when the section
|
|
498
|
+
// is empty and the next heading follows with no blank line between,
|
|
499
|
+
// walking back over the newline(s) can land `e` before `start`. An
|
|
500
|
+
// inverted span makes every mutator's `body.slice(0, start) +
|
|
501
|
+
// sectionBody + body.slice(end)` reassembly duplicate the bytes in
|
|
502
|
+
// `[end, start)`. A zero-length span (`end === start`) is the correct
|
|
503
|
+
// representation of an empty-but-present section.
|
|
504
|
+
end = Math.max(e, start);
|
|
343
505
|
break;
|
|
344
506
|
}
|
|
345
507
|
}
|
|
@@ -470,6 +632,25 @@ function mutateCurrentPositionForAdvance(content, fields, statusDefaults, lastAc
|
|
|
470
632
|
return content;
|
|
471
633
|
let sectionBody = content.slice(span.start, span.end);
|
|
472
634
|
let mutated = false;
|
|
635
|
+
// #3395: Phase is always replaced when a caller passes it — system-derived,
|
|
636
|
+
// not executor-authored (same rule as Plan below). plannedPhaseCore uses
|
|
637
|
+
// this so the transition that declares phase N planned also owns the `Phase:`
|
|
638
|
+
// line the frontmatter resync and `state json` re-derive current_phase from;
|
|
639
|
+
// before, the line survived stale from a previous phase and every
|
|
640
|
+
// body-derived consumer kept reading it (#948 class).
|
|
641
|
+
if (fields.phase) {
|
|
642
|
+
if (/^Phase:/m.test(sectionBody)) {
|
|
643
|
+
sectionBody = sectionBody.replace(/^Phase:.*$/m, `Phase: ${fields.phase}`);
|
|
644
|
+
mutated = true;
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
const replaced = (0, state_document_cjs_1.stateReplaceField)(sectionBody, 'Phase', fields.phase);
|
|
648
|
+
if (replaced !== null) {
|
|
649
|
+
sectionBody = replaced;
|
|
650
|
+
mutated = true;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
473
654
|
if (fields.status) {
|
|
474
655
|
const replaced = (0, state_document_cjs_1.stateReplaceFieldIfTemplate)(sectionBody, 'Status', statusDefaults, fields.status);
|
|
475
656
|
if (replaced !== null && replaced !== sectionBody) {
|
|
@@ -639,6 +820,7 @@ function completePhaseCore(content, intent, deps) {
|
|
|
639
820
|
'current_plan',
|
|
640
821
|
'last_activity',
|
|
641
822
|
'last_activity_desc',
|
|
823
|
+
'stopped_at',
|
|
642
824
|
'progress',
|
|
643
825
|
]) {
|
|
644
826
|
const cls = getFieldClassification(fmKey);
|
|
@@ -683,8 +865,8 @@ function completePhaseCore(content, intent, deps) {
|
|
|
683
865
|
updated.push('Current Phase');
|
|
684
866
|
}
|
|
685
867
|
// Current Phase Name — only written when a next-phase display name is known
|
|
686
|
-
// (#1743/#1695: classified curated/preserve-
|
|
687
|
-
// NOT clear an existing curated value).
|
|
868
|
+
// (#1743/#1695: classified curated/preserve-when-unchanged, so an absent
|
|
869
|
+
// name does NOT clear an existing curated value).
|
|
688
870
|
if (nextPhaseDisplayName) {
|
|
689
871
|
const after = (0, state_document_cjs_1.stateReplaceField)(body, 'Current Phase Name', nextPhaseDisplayName);
|
|
690
872
|
if (after) {
|
|
@@ -729,6 +911,28 @@ function completePhaseCore(content, intent, deps) {
|
|
|
729
911
|
body = ladAfter;
|
|
730
912
|
updated.push('Last Activity Description');
|
|
731
913
|
}
|
|
914
|
+
// Stopped At — #3374: write the continuity line this transition implies.
|
|
915
|
+
// The frontmatter `stopped_at` is a projection of this body line
|
|
916
|
+
// (source: 'body' in FIELD_CLASSIFICATION), and phase completion is exactly
|
|
917
|
+
// the event the line describes — leaving it stale made the post-sync harvest
|
|
918
|
+
// overwrite a fresher frontmatter value with pre-completion prose on every
|
|
919
|
+
// completion (#3374), and left the workflow's later prose refresh as a
|
|
920
|
+
// divergence source. Session-SCOPED replace (stateReplaceFieldInSession):
|
|
921
|
+
// the harvest reads only the session section, so the write must target the
|
|
922
|
+
// same scope — a whole-body replace let a decoy `**Stopped at:**` line in an
|
|
923
|
+
// unrelated section absorb the refresh. Replace-only (no insertion): a
|
|
924
|
+
// STATE.md with no session continuity line keeps its shape, and the
|
|
925
|
+
// unchanged body source then lets the preservation delta keep an existing
|
|
926
|
+
// frontmatter value. Last-phase wording reuses the ADR-2207 status phrase;
|
|
927
|
+
// milestone termination wording stays owned by milestoneCompleteCore.
|
|
928
|
+
const stoppedAtLine = intent.isLastPhase
|
|
929
|
+
? `Phase ${intent.phaseNum} complete — all phases complete`
|
|
930
|
+
: `Phase ${intent.phaseNum} complete${intent.nextPhaseNum ? `, ready to plan Phase ${intent.nextPhaseNum}` : ''}`;
|
|
931
|
+
const stoppedAfter = (0, state_document_cjs_1.stateReplaceFieldInSession)(body, 'Stopped At', 'Stopped at', stoppedAtLine);
|
|
932
|
+
if (stoppedAfter !== body) {
|
|
933
|
+
body = stoppedAfter;
|
|
934
|
+
updated.push('Stopped At');
|
|
935
|
+
}
|
|
732
936
|
// Progress block — re-derive completed/total phases from the roadmap when
|
|
733
937
|
// available (milestone-wide source of truth), then recompute the percent.
|
|
734
938
|
// Only runs when a Completed Phases field exists (the existing guard).
|
|
@@ -744,8 +948,17 @@ function completePhaseCore(content, intent, deps) {
|
|
|
744
948
|
if (derived.totalPhases !== null)
|
|
745
949
|
derivedTotalPhases = derived.totalPhases;
|
|
746
950
|
}
|
|
951
|
+
// #3057 B9: only mark 'Completed Phases' updated when the text actually
|
|
952
|
+
// changed. `stateReplaceField` returns the full (re-)substituted content
|
|
953
|
+
// whenever the field pattern matches, REGARDLESS of whether newCompleted
|
|
954
|
+
// differs from the value already in `body` — so a truthy-only check here
|
|
955
|
+
// marked the field 'updated' even when the roadmap was unavailable and
|
|
956
|
+
// newCompleted is just completedRaw parsed back to itself. That collapsed
|
|
957
|
+
// "recomputed from roadmap" and "left as-is" into the same `updated`
|
|
958
|
+
// signal. Comparing to `body` (the idiom every other field in this
|
|
959
|
+
// function already uses) restores the distinction.
|
|
747
960
|
const completedAfter = (0, state_document_cjs_1.stateReplaceField)(body, 'Completed Phases', String(newCompleted));
|
|
748
|
-
if (completedAfter) {
|
|
961
|
+
if (completedAfter !== null && completedAfter !== body) {
|
|
749
962
|
body = completedAfter;
|
|
750
963
|
updated.push('Completed Phases');
|
|
751
964
|
}
|
|
@@ -753,8 +966,9 @@ function completePhaseCore(content, intent, deps) {
|
|
|
753
966
|
const totalPhases = derivedTotalPhases || (totalRaw ? parseInt(totalRaw, 10) : null);
|
|
754
967
|
if (totalPhases && totalPhases > 0) {
|
|
755
968
|
const newPercent = (0, phase_lifecycle_cjs_1.clampPercent)(newCompleted, totalPhases);
|
|
969
|
+
// Same guard as 'Completed Phases' above, and for the same reason.
|
|
756
970
|
const progAfter = (0, state_document_cjs_1.stateReplaceField)(body, 'Progress', `${newPercent}%`);
|
|
757
|
-
if (progAfter) {
|
|
971
|
+
if (progAfter !== null && progAfter !== body) {
|
|
758
972
|
body = progAfter;
|
|
759
973
|
updated.push('Progress');
|
|
760
974
|
}
|
|
@@ -774,7 +988,10 @@ function completePhaseCore(content, intent, deps) {
|
|
|
774
988
|
* per-phase body fields after plan-phase runs: Status (template-aware — only
|
|
775
989
|
* replaces handler-generated values, preserving executor-authored ones),
|
|
776
990
|
* Total Plans in Phase, Last Activity (template-aware), Last Activity
|
|
777
|
-
* Description, and the ## Current Position section
|
|
991
|
+
* Description, and the ## Current Position section — including its `Phase:`
|
|
992
|
+
* line, which this transition owns (#3395: the line is the body source
|
|
993
|
+
* `current_phase` re-derives from, so it must not survive stale from a
|
|
994
|
+
* previous phase). The adapter wraps this in
|
|
778
995
|
* `readModifyWriteStateMd({ resync: false })` so the milestone-wide progress.*
|
|
779
996
|
* frontmatter is NOT re-derived from a half-planned disk snapshot (#500 RC1).
|
|
780
997
|
*
|
|
@@ -827,9 +1044,20 @@ function plannedPhaseCore(content, intent, deps) {
|
|
|
827
1044
|
body = ladResult;
|
|
828
1045
|
updated.push('Last Activity Description');
|
|
829
1046
|
}
|
|
830
|
-
// ## Current Position section — Status + Last activity
|
|
1047
|
+
// ## Current Position section — Phase + Status + Last activity.
|
|
1048
|
+
// #3395: plannedPhaseCore owns the `Phase:` line for the same reason
|
|
1049
|
+
// beginPhaseCore/completePhaseCore do — it is the body source the frontmatter
|
|
1050
|
+
// resync and `state json` re-derive `current_phase` from. Before, a stale
|
|
1051
|
+
// line from a previous phase survived this transition and every
|
|
1052
|
+
// body-derived consumer kept reading it (the write path was already
|
|
1053
|
+
// protected by the #3258 preserve-when-unchanged row; the source itself was
|
|
1054
|
+
// never refreshed). The label mirrors beginPhaseCore's `N (Name) — EXECUTING`
|
|
1055
|
+
// convention with this transition's status vocabulary ("Ready to execute").
|
|
1056
|
+
// Phase is system-derived, always replaced (Knuth invariant does not apply);
|
|
1057
|
+
// Status / Last activity stay template-aware.
|
|
831
1058
|
const beforePos = body;
|
|
832
1059
|
body = mutateCurrentPositionForAdvance(body, {
|
|
1060
|
+
phase: `${intent.phaseNumber}${intent.phaseName ? ` (${intent.phaseName})` : ''} — READY TO EXECUTE`,
|
|
833
1061
|
status: 'Ready to execute',
|
|
834
1062
|
lastActivity: `${today} — Phase ${intent.phaseNumber} planning complete`,
|
|
835
1063
|
}, statusDefaults, lastActivityDefaults);
|
|
@@ -864,10 +1092,11 @@ function plannedPhaseCore(content, intent, deps) {
|
|
|
864
1092
|
* preserved.
|
|
865
1093
|
*
|
|
866
1094
|
* This is a destructive reset intent: it intentionally overwrites the curated
|
|
867
|
-
* `progress` / `current_phase_name`
|
|
868
|
-
* a new milestone starts from zero. That is the intent's
|
|
869
|
-
* violation of the field-classification table — the table
|
|
870
|
-
* state RMW transitions; a milestone boundary is an
|
|
1095
|
+
* `progress` (preserve-always) / `current_phase_name` (preserve-when-unchanged)
|
|
1096
|
+
* fields because a new milestone starts from zero. That is the intent's
|
|
1097
|
+
* contract, not a violation of the field-classification table — the table
|
|
1098
|
+
* governs the steady-state RMW transitions; a milestone boundary is an
|
|
1099
|
+
* explicit reset.
|
|
871
1100
|
*
|
|
872
1101
|
* The adapter wraps this in `acquireStateLock` + `platformWriteSync` (NOT
|
|
873
1102
|
* `readModifyWriteStateMd`) because milestoneSwitch rebuilds frontmatter
|
|
@@ -1085,13 +1314,49 @@ function milestoneCompleteCore(content, intent, deps) {
|
|
|
1085
1314
|
* Apply a `patch` transition to STATE.md content.
|
|
1086
1315
|
*
|
|
1087
1316
|
* Migrates `cmdStatePatch` (state.cts) onto the substrate. Applies each
|
|
1088
|
-
* caller-supplied `{field: value}` pair
|
|
1089
|
-
*
|
|
1090
|
-
*
|
|
1317
|
+
* caller-supplied `{field: value}` pair, resolved BODY-FIRST:
|
|
1318
|
+
*
|
|
1319
|
+
* - A key that resolves against the STRIPPED body (via `stateReplaceField`,
|
|
1320
|
+
* case-insensitive on the field name) is applied there and reported
|
|
1321
|
+
* `updated` — this is the legitimate, documented case (display-cased body
|
|
1322
|
+
* fields — Status, Current Plan, Phase — which are never frontmatter
|
|
1323
|
+
* keys). It wins deterministically even when the same key also happens to
|
|
1324
|
+
* exist as a parsed frontmatter key (e.g. `status` matches both the
|
|
1325
|
+
* frontmatter key and a `Status:` body line) — frontmatter is inert for
|
|
1326
|
+
* that key.
|
|
1327
|
+
* - Only when the body has no match is the key checked against parsed
|
|
1328
|
+
* frontmatter (determined structurally, never by a naming heuristic), and
|
|
1329
|
+
* routed through the seam: `FIELD_CLASSIFICATION` governs it. A CLASSIFIED
|
|
1330
|
+
* key (has a row, e.g. `current_phase`, `current_phase_name`) is NOT
|
|
1331
|
+
* writable by an arbitrary patch — policy owns it — and is reported
|
|
1332
|
+
* `failed`. An UNCLASSIFIED key (no row, e.g. a custom `risk_level`) is a
|
|
1333
|
+
* pass-through per Phase 1 behavior-table row 19 ("field absent from
|
|
1334
|
+
* FIELD_CLASSIFICATION → untouched pass-through"): it is applied directly
|
|
1335
|
+
* to the frontmatter object before reassembly and reported `updated`.
|
|
1336
|
+
* - A key matching neither the body nor the frontmatter is reported `failed`.
|
|
1337
|
+
*
|
|
1338
|
+
* ADR-3408 §8.3(b): this used to run `stateReplaceField` over the FULL
|
|
1339
|
+
* document (body + frontmatter), which — because `field` is an arbitrary,
|
|
1340
|
+
* caller-supplied string, unlike every other `stateReplaceField` call site in
|
|
1341
|
+
* this file, which passes a fixed Title-Case string literal that can never
|
|
1342
|
+
* collide with a lowercase/snake_case YAML key — let a frontmatter-shaped
|
|
1343
|
+
* patch key (e.g. `status`, `current_phase`) match and rewrite the YAML
|
|
1344
|
+
* frontmatter block directly via `stateReplaceField`'s case-insensitive
|
|
1345
|
+
* `^field:` line pattern, entirely outside `FIELD_CLASSIFICATION` and the
|
|
1346
|
+
* write-seam preservation policy: a second, undeclared writer. The fix is
|
|
1347
|
+
* that a CLASSIFIED frontmatter key no longer writes outside the declared
|
|
1348
|
+
* policy table — not that every frontmatter-shaped key stops working.
|
|
1349
|
+
* `.gsd/phase/refactor-3469-one-write-seam/40-design.md` row 9 requires
|
|
1350
|
+
* frontmatter changes to route through the seam (still work, governed by
|
|
1351
|
+
* FIELD_CLASSIFICATION), not to stop working outright. Body-shaped keys
|
|
1352
|
+
* (`Status`, `Current Plan`, `Phase`, ...) are the LEGITIMATE case and are
|
|
1353
|
+
* unaffected — they were always matched against the body text, and still are.
|
|
1091
1354
|
*
|
|
1092
1355
|
* The curated-field preservation that fixes #1743/#1695 is NOT in this core —
|
|
1093
|
-
* it lives in
|
|
1094
|
-
* `
|
|
1356
|
+
* it lives in the write seam's post-sync delta (table-driven via
|
|
1357
|
+
* `current_phase_name`'s `preserve-when-unchanged` row, ADR-3408 §8.1 —
|
|
1358
|
+
* reclassified from `preserve-always` in #3468 to match its long-standing,
|
|
1359
|
+
* delta-gated behavior).
|
|
1095
1360
|
* `patch` consulting the table "refuses to overwrite" curated fields implicitly:
|
|
1096
1361
|
* when the patch does not change a curated field's body source line, the
|
|
1097
1362
|
* existing frontmatter value wins over the sync re-derivation. The adapter
|
|
@@ -1100,19 +1365,55 @@ function milestoneCompleteCore(content, intent, deps) {
|
|
|
1100
1365
|
* `data.updated` / `data.failed` mirror the pre-migration CLI output shape.
|
|
1101
1366
|
*/
|
|
1102
1367
|
function patchCore(content, intent) {
|
|
1368
|
+
const existingFm = extractFrontmatter(content);
|
|
1369
|
+
const hasFrontmatter = Object.keys(existingFm).length > 0;
|
|
1370
|
+
let body = stripFrontmatter(content);
|
|
1371
|
+
const fm = { ...existingFm };
|
|
1103
1372
|
const updated = [];
|
|
1104
1373
|
const failed = [];
|
|
1105
|
-
let result = content;
|
|
1106
1374
|
for (const [field, value] of Object.entries(intent.patches)) {
|
|
1107
|
-
|
|
1375
|
+
// Body-first: a key that resolves against a body field is the
|
|
1376
|
+
// legitimate, documented case (display-cased body fields — Status,
|
|
1377
|
+
// Current Plan, Phase — are never frontmatter keys) and wins
|
|
1378
|
+
// deterministically even when the same key also happens to exist as a
|
|
1379
|
+
// frontmatter key (case-insensitively, via stateReplaceField's
|
|
1380
|
+
// `^field:` pattern — e.g. `status` matching both the frontmatter key
|
|
1381
|
+
// and a `Status:` body line). Frontmatter is only consulted when the
|
|
1382
|
+
// body has no match for this key.
|
|
1383
|
+
const replaced = (0, state_document_cjs_1.stateReplaceField)(body, field, value);
|
|
1108
1384
|
if (replaced !== null) {
|
|
1109
|
-
|
|
1385
|
+
body = replaced;
|
|
1110
1386
|
updated.push(field);
|
|
1387
|
+
continue;
|
|
1111
1388
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1389
|
+
if (Object.prototype.hasOwnProperty.call(existingFm, field)) {
|
|
1390
|
+
// Frontmatter-shaped key: route through the seam. A classified field
|
|
1391
|
+
// is policy-owned — a raw patch may not bypass it. An unclassified
|
|
1392
|
+
// field is an untouched pass-through (behavior-table row 19).
|
|
1393
|
+
if (getFieldClassification(field) !== null) {
|
|
1394
|
+
failed.push(field);
|
|
1395
|
+
}
|
|
1396
|
+
else {
|
|
1397
|
+
fm[field] = value;
|
|
1398
|
+
updated.push(field);
|
|
1399
|
+
}
|
|
1400
|
+
continue;
|
|
1114
1401
|
}
|
|
1115
|
-
|
|
1402
|
+
failed.push(field);
|
|
1403
|
+
}
|
|
1404
|
+
if (updated.length === 0) {
|
|
1405
|
+
// No field matched — return `content` VERBATIM (mirrors `updateCore`'s
|
|
1406
|
+
// null-result branch): reassembling via stripFrontmatter/
|
|
1407
|
+
// reconstructFrontmatter even when nothing changed can round-trip the
|
|
1408
|
+
// frontmatter block to different bytes than the original (key order,
|
|
1409
|
+
// formatting), which would falsely defeat `readModifyWriteStateMd`'s
|
|
1410
|
+
// #948 no-op write guard for every patch that updates nothing, not just
|
|
1411
|
+
// a frontmatter-shaped one.
|
|
1412
|
+
return { content, updated, data: { updated, failed } };
|
|
1413
|
+
}
|
|
1414
|
+
const result = hasFrontmatter
|
|
1415
|
+
? `---\n${reconstructFrontmatter(fm)}\n---\n\n${body}`
|
|
1416
|
+
: body;
|
|
1116
1417
|
return { content: result, updated, data: { updated, failed } };
|
|
1117
1418
|
}
|
|
1118
1419
|
// ----------------------------------------------------------------------------
|
|
@@ -1352,13 +1653,14 @@ function truncateForLog(s) {
|
|
|
1352
1653
|
function rebuildCore(content, _intent, deps) {
|
|
1353
1654
|
const timestamp = deps.clock.nowIso();
|
|
1354
1655
|
const log = [];
|
|
1656
|
+
const phaseInventoryScan = { failed: false, reason: null };
|
|
1355
1657
|
let modified = content;
|
|
1356
1658
|
// §2 Decision: re-derive derived sections, preserve others. Order is
|
|
1357
1659
|
// oldest-section-first so log entries appear in body order.
|
|
1358
1660
|
// sourcePath threaded so `state rebuild --dry-run` names the file: that branch reads STATE.md
|
|
1359
1661
|
// directly rather than through readModifyWriteStateMd, so nothing upstream has named it yet.
|
|
1360
1662
|
modified = reconcileCurrentPosition(modified, timestamp, log, deps.sourcePath);
|
|
1361
|
-
modified = reconcileByPhaseTable(modified, deps, timestamp, log);
|
|
1663
|
+
modified = reconcileByPhaseTable(modified, deps, timestamp, log, phaseInventoryScan);
|
|
1362
1664
|
modified = stripTemplatePlaceholders(modified, timestamp, log);
|
|
1363
1665
|
modified = deduplicateSessionArchive(modified, timestamp, log);
|
|
1364
1666
|
// §3 + §4: append the audit log ONLY when mutations occurred. The
|
|
@@ -1375,6 +1677,11 @@ function rebuildCore(content, _intent, deps) {
|
|
|
1375
1677
|
mutated: log.length > 0,
|
|
1376
1678
|
mutations: log.length,
|
|
1377
1679
|
log,
|
|
1680
|
+
// #3057 B1: distinguishable from a clean "nothing to reconcile" — a
|
|
1681
|
+
// failed phase-inventory scan means the by-phase table was NOT
|
|
1682
|
+
// verified against disk, even though `mutated` may still be false.
|
|
1683
|
+
phase_inventory_scan_failed: phaseInventoryScan.failed,
|
|
1684
|
+
...(phaseInventoryScan.reason !== null ? { phase_inventory_scan_reason: phaseInventoryScan.reason } : {}),
|
|
1378
1685
|
},
|
|
1379
1686
|
};
|
|
1380
1687
|
}
|
|
@@ -1451,12 +1758,24 @@ function reconcileCurrentPosition(content, timestamp, log, sourcePath) {
|
|
|
1451
1758
|
* Leaky-Abstractions guard (ADR-1817 §1): when `phaseInventoryProvider` is
|
|
1452
1759
|
* absent (no disk scan wired), this step is a no-op. The core stays pure and
|
|
1453
1760
|
* testable without disk I/O.
|
|
1761
|
+
*
|
|
1762
|
+
* A DIFFERENT case is a scan that ran but failed (`ok:false`): that is NOT a
|
|
1763
|
+
* no-op-equivalent "nothing to reconcile" — the table is left untouched (we
|
|
1764
|
+
* have no trustworthy inventory to reconcile against) but the failure is
|
|
1765
|
+
* recorded into `meta` so the caller (`rebuildCore`) can surface it instead
|
|
1766
|
+
* of reporting a clean, fully-reconciled rebuild (#3057 B1).
|
|
1454
1767
|
*/
|
|
1455
|
-
function reconcileByPhaseTable(content, deps, timestamp, log) {
|
|
1768
|
+
function reconcileByPhaseTable(content, deps, timestamp, log, meta) {
|
|
1456
1769
|
if (!deps.phaseInventoryProvider)
|
|
1457
1770
|
return content;
|
|
1458
|
-
const
|
|
1459
|
-
if (!
|
|
1771
|
+
const result = deps.phaseInventoryProvider();
|
|
1772
|
+
if (!result.ok) {
|
|
1773
|
+
meta.failed = true;
|
|
1774
|
+
meta.reason = result.reason;
|
|
1775
|
+
return content; // cannot reconcile without a trustworthy inventory — leave the table as-is
|
|
1776
|
+
}
|
|
1777
|
+
const inventory = result.phases;
|
|
1778
|
+
if (inventory.length === 0)
|
|
1460
1779
|
return content;
|
|
1461
1780
|
// The canonical table shape (from gsd-core/templates/state.md):
|
|
1462
1781
|
// | Phase | Plans | Total | Avg/Plan |
|