@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
|
@@ -16,6 +16,7 @@ const ioMod = require("./io.cjs");
|
|
|
16
16
|
const { output, error } = ioMod;
|
|
17
17
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
18
18
|
const validate_cjs_1 = require("./validate.cjs");
|
|
19
|
+
const text_lines_cjs_1 = require("./text-lines.cjs");
|
|
19
20
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
20
21
|
const unusableInputMod = require("./unusable-input.cjs");
|
|
21
22
|
const { UNUSABLE_REASON, warnUnusableInput } = unusableInputMod;
|
|
@@ -83,7 +84,7 @@ const UNTERMINATED_KEY_THRESHOLD = 2;
|
|
|
83
84
|
* has a false-positive class the other closes.
|
|
84
85
|
*/
|
|
85
86
|
function isFrontmatterShaped(region) {
|
|
86
|
-
const lines =
|
|
87
|
+
const lines = (0, text_lines_cjs_1.splitLines)(region).filter((line) => line.trim() !== '');
|
|
87
88
|
if (lines.length === 0)
|
|
88
89
|
return false;
|
|
89
90
|
return lines.every((line) => (/^\s*[a-zA-Z0-9_-]+:/.test(line) // key: value
|
|
@@ -91,6 +92,80 @@ function isFrontmatterShaped(region) {
|
|
|
91
92
|
|| /^\s+\S/.test(line) // indented continuation of a nested value
|
|
92
93
|
));
|
|
93
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* #3257: a Symbol-keyed channel that carries full-line (column-0 `#`) YAML
|
|
97
|
+
* comments through a parse → reconstruct round-trip. Comments are otherwise
|
|
98
|
+
* unrepresentable on the Frontmatter object (Record<string, ...>) and were
|
|
99
|
+
* silently dropped by reconstructFrontmatter. The Symbol is invisible to
|
|
100
|
+
* Object.entries / Object.keys / JSON.stringify / for-in, so every existing
|
|
101
|
+
* reader is unchanged; only reconstructFrontmatter reads it. Leading comments
|
|
102
|
+
* are attached to the top-level key that follows them; comments after the last
|
|
103
|
+
* key go to `trailing`. Only set when a comment is actually seen, so comment-less
|
|
104
|
+
* frontmatter parses byte-identically to before.
|
|
105
|
+
*/
|
|
106
|
+
const FULL_LINE_COMMENTS = Symbol('fullLineComments');
|
|
107
|
+
/**
|
|
108
|
+
* Unescape the interior of a YAML double-quoted scalar — the exact inverse of
|
|
109
|
+
* `escapeDoubleQuoted` (#3497). The writer has escaped `\`/`"`/`\n`/`\t`/`\r`/
|
|
110
|
+
* `\xHH` since #1779, but the reader only stripped the delimiters, so
|
|
111
|
+
* parse(serialize(x)) ≠ x for any quoted scalar carrying a `"` or `\`: each
|
|
112
|
+
* read-modify-write round-trip doubled the backslashes (b → 2b+1), growing a
|
|
113
|
+
* repeatedly-synced field — and its document — without bound until tooling
|
|
114
|
+
* OOMed. Recognized escapes decode per YAML double-quoted semantics; an
|
|
115
|
+
* unrecognized `\c` is kept literally (backslash + char), matching the
|
|
116
|
+
* strip-only behavior hand-authored files had before this fix.
|
|
117
|
+
*/
|
|
118
|
+
function unescapeDoubleQuoted(s) {
|
|
119
|
+
let out = '';
|
|
120
|
+
for (let i = 0; i < s.length; i++) {
|
|
121
|
+
const ch = s[i];
|
|
122
|
+
if (ch !== '\\' || i === s.length - 1) {
|
|
123
|
+
out += ch;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const next = s[++i];
|
|
127
|
+
if (next === '\\' || next === '"') {
|
|
128
|
+
out += next;
|
|
129
|
+
}
|
|
130
|
+
else if (next === 'n') {
|
|
131
|
+
out += '\n';
|
|
132
|
+
}
|
|
133
|
+
else if (next === 't') {
|
|
134
|
+
out += '\t';
|
|
135
|
+
}
|
|
136
|
+
else if (next === 'r') {
|
|
137
|
+
out += '\r';
|
|
138
|
+
}
|
|
139
|
+
else if (next === 'x') {
|
|
140
|
+
const hex = s.slice(i + 1, i + 3);
|
|
141
|
+
if (/^[0-9a-fA-F]{2}$/.test(hex)) {
|
|
142
|
+
out += String.fromCharCode(parseInt(hex, 16));
|
|
143
|
+
i += 2;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
out += '\\x'; // not \xHH — keep literally
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
out += '\\' + next; // unrecognized escape — keep literally
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Strip the quote delimiters off a parsed YAML scalar, un-escaping the interior
|
|
157
|
+
* when the scalar is double-quoted (#3497 — the parse-side complement of
|
|
158
|
+
* `escapeDoubleQuoted`). Single-quoted scalars keep the historical strip-only
|
|
159
|
+
* behavior (the writer never emits them; `''` → `'` folding is out of scope).
|
|
160
|
+
* A scalar wrapped in double quotes un-escapes; anything else keeps the exact
|
|
161
|
+
* prior delimiter-strip behavior, including a stray unpaired boundary quote.
|
|
162
|
+
*/
|
|
163
|
+
function parseQuotedScalar(value) {
|
|
164
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
165
|
+
return unescapeDoubleQuoted(value.slice(1, -1));
|
|
166
|
+
}
|
|
167
|
+
return value.replace(/^["']|["']$/g, '');
|
|
168
|
+
}
|
|
94
169
|
/**
|
|
95
170
|
* Parse one already-delimited YAML region into a Frontmatter object.
|
|
96
171
|
*
|
|
@@ -100,12 +175,20 @@ function isFrontmatterShaped(region) {
|
|
|
100
175
|
*/
|
|
101
176
|
function parseYamlRegion(yaml) {
|
|
102
177
|
const frontmatter = {};
|
|
103
|
-
const lines =
|
|
178
|
+
const lines = (0, text_lines_cjs_1.splitLines)(yaml);
|
|
179
|
+
// #3257: pending column-0 full-line comments, attached to the next top-level key.
|
|
180
|
+
let pendingComments = [];
|
|
181
|
+
let commentChannel;
|
|
104
182
|
const stack = [{ obj: frontmatter, key: null, indent: -1 }];
|
|
105
183
|
for (const line of lines) {
|
|
106
184
|
// Skip empty lines
|
|
107
185
|
if (line.trim() === '')
|
|
108
186
|
continue;
|
|
187
|
+
// #3257: capture column-0 full-line comments; attach them to the next top-level key.
|
|
188
|
+
if (/^#/.test(line)) {
|
|
189
|
+
pendingComments.push(line);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
109
192
|
// Calculate indentation (number of leading spaces)
|
|
110
193
|
const indentMatch = line.match(/^(\s*)/);
|
|
111
194
|
const indent = indentMatch ? indentMatch[1].length : 0;
|
|
@@ -118,6 +201,13 @@ function parseYamlRegion(yaml) {
|
|
|
118
201
|
const keyMatch = line.match(/^(\s*)([a-zA-Z0-9_-]+):\s*(.*)/);
|
|
119
202
|
if (keyMatch) {
|
|
120
203
|
const key = keyMatch[2];
|
|
204
|
+
// #3257: attach any pending comments to this (top-level) key.
|
|
205
|
+
if (pendingComments.length) {
|
|
206
|
+
if (!commentChannel)
|
|
207
|
+
commentChannel = { leading: {}, trailing: [] };
|
|
208
|
+
commentChannel.leading[key] = pendingComments;
|
|
209
|
+
pendingComments = [];
|
|
210
|
+
}
|
|
121
211
|
const value = keyMatch[3].trim();
|
|
122
212
|
if (value === '' || value === '[') {
|
|
123
213
|
// Key with no value or opening bracket — could be nested object or array
|
|
@@ -134,13 +224,13 @@ function parseYamlRegion(yaml) {
|
|
|
134
224
|
}
|
|
135
225
|
else {
|
|
136
226
|
// Simple key: value
|
|
137
|
-
current.obj[key] = value
|
|
227
|
+
current.obj[key] = parseQuotedScalar(value);
|
|
138
228
|
current.key = null;
|
|
139
229
|
}
|
|
140
230
|
}
|
|
141
231
|
else if (line.trim().startsWith('- ')) {
|
|
142
232
|
// Array item
|
|
143
|
-
const itemValue = line.trim().slice(2)
|
|
233
|
+
const itemValue = parseQuotedScalar(line.trim().slice(2));
|
|
144
234
|
// If current context is an empty object, convert to array
|
|
145
235
|
if (typeof current.obj === 'object' && !Array.isArray(current.obj) && Object.keys(current.obj).length === 0) {
|
|
146
236
|
// Find the key in parent that points to this object and convert it
|
|
@@ -160,6 +250,15 @@ function parseYamlRegion(yaml) {
|
|
|
160
250
|
}
|
|
161
251
|
}
|
|
162
252
|
}
|
|
253
|
+
// #3257: trailing comments (after the last key) + attach the channel if any comment was seen.
|
|
254
|
+
if (pendingComments.length) {
|
|
255
|
+
if (!commentChannel)
|
|
256
|
+
commentChannel = { leading: {}, trailing: [] };
|
|
257
|
+
commentChannel.trailing = pendingComments;
|
|
258
|
+
}
|
|
259
|
+
if (commentChannel) {
|
|
260
|
+
frontmatter[FULL_LINE_COMMENTS] = commentChannel;
|
|
261
|
+
}
|
|
163
262
|
return frontmatter;
|
|
164
263
|
}
|
|
165
264
|
/**
|
|
@@ -195,6 +294,18 @@ function parseYamlRegion(yaml) {
|
|
|
195
294
|
* hold only an in-memory string; those dedup on a content digest instead.
|
|
196
295
|
*/
|
|
197
296
|
function extractFrontmatter(content, sourcePath) {
|
|
297
|
+
// #2977: tolerate a single leading UTF-8 BOM (\uFEFF), which Windows tooling
|
|
298
|
+
// (PowerShell `>`/`Out-File` on PS 5.1, several editors) writes by default. Without this
|
|
299
|
+
// strip, the byte-0 `startsWith('---')` fence check below fails on the BOM and the whole
|
|
300
|
+
// parse collapses to {} — every frontmatter field silently disappears, and the engine
|
|
301
|
+
// proceeds as though the file had no frontmatter at all. The BOM is a single codepoint;
|
|
302
|
+
// stripping it here restores byte-0 alignment so the rest of the function is unchanged.
|
|
303
|
+
// Scope: BOM only. Arbitrary non-BOM content before the fence (leading whitespace/blank
|
|
304
|
+
// line/comment) is a separate product-intent decision (tolerate vs diagnose) left to a
|
|
305
|
+
// future change — this fix does not broaden the byte-0 fence rule beyond the BOM.
|
|
306
|
+
if (content.charCodeAt(0) === 0xFEFF) {
|
|
307
|
+
content = content.slice(1);
|
|
308
|
+
}
|
|
198
309
|
// Match frontmatter only at byte 0 — a `---` block later in the document
|
|
199
310
|
// body (YAML examples, horizontal rules) must never be treated as frontmatter.
|
|
200
311
|
const headerEnd = content.startsWith('---\r\n') ? 5 : content.startsWith('---\n') ? 4 : -1;
|
|
@@ -261,9 +372,17 @@ function scalarNeedsDoubleQuoting(s) {
|
|
|
261
372
|
}
|
|
262
373
|
function reconstructFrontmatter(obj) {
|
|
263
374
|
const lines = [];
|
|
375
|
+
// #3257: read the full-line-comment channel (set by parseYamlRegion when comments
|
|
376
|
+
// were present). Object.entries skips the Symbol key, so the data loop is unchanged.
|
|
377
|
+
const commentChannel = obj[FULL_LINE_COMMENTS];
|
|
264
378
|
for (const [key, value] of Object.entries(obj)) {
|
|
265
379
|
if (value === null || value === undefined)
|
|
266
380
|
continue;
|
|
381
|
+
// #3257: re-emit this key's leading full-line comments before the key itself.
|
|
382
|
+
const leading = commentChannel?.leading[key];
|
|
383
|
+
if (leading)
|
|
384
|
+
for (const c of leading)
|
|
385
|
+
lines.push(c);
|
|
267
386
|
if (Array.isArray(value)) {
|
|
268
387
|
if (value.length === 0) {
|
|
269
388
|
lines.push(`${key}: []`);
|
|
@@ -336,8 +455,35 @@ function reconstructFrontmatter(obj) {
|
|
|
336
455
|
}
|
|
337
456
|
}
|
|
338
457
|
}
|
|
458
|
+
// #3257: re-emit any trailing full-line comments (those after the last key).
|
|
459
|
+
if (commentChannel?.trailing?.length) {
|
|
460
|
+
for (const c of commentChannel.trailing)
|
|
461
|
+
lines.push(c);
|
|
462
|
+
}
|
|
339
463
|
return lines.join('\n');
|
|
340
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* #3257: copy the full-line-comment channel from `source` onto `target`, filtering
|
|
467
|
+
* `leading` to keys still present in `target` (a deleted key's annotation goes with
|
|
468
|
+
* it — AC5). No-op when `source` carries no channel. Consumers that rebuild their
|
|
469
|
+
* target object fresh (syncStateFrontmatter builds derivedFm via buildStateFrontmatter
|
|
470
|
+
* and copies keys with Object.keys, which skips the Symbol) MUST call this before
|
|
471
|
+
* reconstructFrontmatter, or the channel parseYamlRegion attached to the extracted
|
|
472
|
+
* source is lost.
|
|
473
|
+
*/
|
|
474
|
+
function propagateCommentChannel(source, target) {
|
|
475
|
+
const channel = source[FULL_LINE_COMMENTS];
|
|
476
|
+
if (!channel)
|
|
477
|
+
return;
|
|
478
|
+
const filtered = { leading: {}, trailing: channel.trailing };
|
|
479
|
+
for (const [key, comments] of Object.entries(channel.leading)) {
|
|
480
|
+
if (key in target)
|
|
481
|
+
filtered.leading[key] = comments;
|
|
482
|
+
}
|
|
483
|
+
if (filtered.trailing.length || Object.keys(filtered.leading).length) {
|
|
484
|
+
target[FULL_LINE_COMMENTS] = filtered;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
341
487
|
/**
|
|
342
488
|
* Slice a frontmatter YAML body into per-top-level-key raw text segments. Each segment
|
|
343
489
|
* runs from a column-0 `key:` line through the line before the next column-0 key (or the
|
|
@@ -347,7 +493,7 @@ function reconstructFrontmatter(obj) {
|
|
|
347
493
|
* not modify (e.g. must_haves.artifacts / .prohibitions).
|
|
348
494
|
*/
|
|
349
495
|
function sliceTopLevelFrontmatterSegments(yaml) {
|
|
350
|
-
const lines =
|
|
496
|
+
const lines = (0, text_lines_cjs_1.splitLines)(yaml);
|
|
351
497
|
const segments = [];
|
|
352
498
|
let current = null;
|
|
353
499
|
for (const line of lines) {
|
|
@@ -411,7 +557,7 @@ function spliceFrontmatter(content, newObj) {
|
|
|
411
557
|
// unrelated `must_haves` block. Keys absent from the original (genuinely new) are
|
|
412
558
|
// regenerated and appended; keys absent from `newObj` are preserved (never silently
|
|
413
559
|
// deleted by a set/merge).
|
|
414
|
-
const fmLines =
|
|
560
|
+
const fmLines = (0, text_lines_cjs_1.splitLines)(fmBlock);
|
|
415
561
|
const inner = fmLines.slice(1, -1).join('\n'); // drop the opening `---` and closing `---`
|
|
416
562
|
let originalParsed;
|
|
417
563
|
try {
|
|
@@ -492,27 +638,27 @@ function parseMustHavesBlock(content, blockName) {
|
|
|
492
638
|
if (!fmMatch)
|
|
493
639
|
return [];
|
|
494
640
|
const yaml = fmMatch[1];
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
641
|
+
const yamlLines = (0, text_lines_cjs_1.splitLines)(yaml);
|
|
642
|
+
// Find must_haves: first to detect its indentation level. Split-then-scan
|
|
643
|
+
// (rather than a whole-string /m match) so a CRLF or blank-line boundary
|
|
644
|
+
// can never be absorbed into the indent capture (#3360) — see
|
|
645
|
+
// .gsd/phase/chore-3413-text-lines-seam/40-design.md.
|
|
646
|
+
const mustHavesLinePattern = /^(\s*)must_haves:\s*$/;
|
|
647
|
+
const mustHavesLineIndex = yamlLines.findIndex((line) => mustHavesLinePattern.test(line));
|
|
648
|
+
if (mustHavesLineIndex === -1)
|
|
498
649
|
return [];
|
|
499
|
-
const mustHavesIndent =
|
|
650
|
+
const mustHavesIndent = yamlLines[mustHavesLineIndex].match(/^(\s*)/)[1].length;
|
|
500
651
|
// Find the block (e.g., "truths:", "artifacts:", "key_links:") under must_haves
|
|
501
652
|
// It must be indented more than must_haves but we detect the actual indent dynamically
|
|
502
|
-
const
|
|
503
|
-
const
|
|
504
|
-
if (
|
|
653
|
+
const blockLinePattern = new RegExp(`^(\\s+)${blockName}:\\s*$`);
|
|
654
|
+
const blockLineIndex = yamlLines.findIndex((line) => blockLinePattern.test(line));
|
|
655
|
+
if (blockLineIndex === -1)
|
|
505
656
|
return [];
|
|
506
|
-
const blockIndent =
|
|
657
|
+
const blockIndent = yamlLines[blockLineIndex].match(/^(\s*)/)[1].length;
|
|
507
658
|
// The block must be nested under must_haves (more indented)
|
|
508
659
|
if (blockIndent <= mustHavesIndent)
|
|
509
660
|
return [];
|
|
510
|
-
|
|
511
|
-
const blockStart = yaml.indexOf(blockMatch[0]);
|
|
512
|
-
if (blockStart === -1)
|
|
513
|
-
return [];
|
|
514
|
-
const afterBlock = yaml.slice(blockStart);
|
|
515
|
-
const blockLines = afterBlock.split(/\r?\n/).slice(1); // skip the header line
|
|
661
|
+
const blockLines = yamlLines.slice(blockLineIndex + 1); // skip the header line
|
|
516
662
|
// List items are indented one level deeper than blockIndent
|
|
517
663
|
// Continuation KVs are indented one level deeper than list items
|
|
518
664
|
const items = [];
|
|
@@ -607,29 +753,64 @@ function parseMustHavesBlock(content, blockName) {
|
|
|
607
753
|
return items;
|
|
608
754
|
}
|
|
609
755
|
// ─── Frontmatter CRUD commands ────────────────────────────────────────────────
|
|
756
|
+
// Shared base for 'plan' and 'plan-gap-closure' below — a plain array reference (not
|
|
757
|
+
// FRONTMATTER_SCHEMAS.plan.required) because the object literal that defines
|
|
758
|
+
// FRONTMATTER_SCHEMAS cannot refer to itself mid-initialization (TDZ).
|
|
759
|
+
const PLAN_REQUIRED_FIELDS = ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'];
|
|
760
|
+
// `requiredValues` is optional per schema: when a field name is a key here, the
|
|
761
|
+
// field must be PRESENT AND strictly equal (===) to the given value to satisfy
|
|
762
|
+
// the schema — presence alone is not enough. Every other required field (no
|
|
763
|
+
// entry in requiredValues) keeps the original presence-only contract.
|
|
610
764
|
const FRONTMATTER_SCHEMAS = {
|
|
611
|
-
plan: { required:
|
|
765
|
+
plan: { required: PLAN_REQUIRED_FIELDS },
|
|
766
|
+
// #2847: gap-closure plans carry every 'plan' field PLUS gap_closure — the flag
|
|
767
|
+
// execute-phase --gaps-only filters on. A separate schema (not a change to
|
|
768
|
+
// 'plan') so standard/reviews-mode plans stay unaffected: they validate against
|
|
769
|
+
// 'plan' and are never required to declare or be checked for gap_closure.
|
|
770
|
+
// Derived from PLAN_REQUIRED_FIELDS (never hand-duplicated) so the two can't drift.
|
|
771
|
+
//
|
|
772
|
+
// requiredValues.gap_closure = true (not just presence): --gaps-only filters
|
|
773
|
+
// strictly on gap_closure === true (execute-phase.md, partial-wave.md), so a
|
|
774
|
+
// plan carrying `gap_closure: false` would pass a presence-only check and
|
|
775
|
+
// still be silently skipped at execute time — the exact symptom #2847
|
|
776
|
+
// reports, one value away. Presence-only was flagged in review as a live
|
|
777
|
+
// reproduction of the bug this schema exists to close.
|
|
778
|
+
'plan-gap-closure': {
|
|
779
|
+
required: [...PLAN_REQUIRED_FIELDS, 'gap_closure'],
|
|
780
|
+
// extractFrontmatter parses every scalar as a string (FrontmatterValue has
|
|
781
|
+
// no boolean member — `gap_closure: true` in YAML becomes the JS string
|
|
782
|
+
// "true", not the boolean true), so the required value is the string here.
|
|
783
|
+
requiredValues: { gap_closure: 'true' },
|
|
784
|
+
},
|
|
612
785
|
summary: { required: ['phase', 'plan', 'subsystem', 'tags', 'duration', 'completed'] },
|
|
613
786
|
verification: { required: ['phase', 'verified', 'status', 'score'] },
|
|
614
787
|
};
|
|
615
788
|
/**
|
|
616
|
-
* Strip
|
|
789
|
+
* Strip frontmatter blocks from the start of `content`.
|
|
790
|
+
*
|
|
791
|
+
* Handles CRLF line endings and, by default, multiple stacked blocks
|
|
792
|
+
* (corruption recovery): greedily strips consecutive `---...---` blocks
|
|
793
|
+
* separated by optional whitespace, so a doubled/tripled frontmatter header
|
|
794
|
+
* (e.g. from a botched merge) is fully removed, not just the first block.
|
|
617
795
|
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
796
|
+
* Pass `{ once: true }` to stop after the first block. Callers whose input is
|
|
797
|
+
* an arbitrary user-authored document — rather than a GSD artefact with a
|
|
798
|
+
* known doubling failure mode — need this: a body that opens with a
|
|
799
|
+
* thematic-break-delimited section is lexically indistinguishable from a
|
|
800
|
+
* second frontmatter block, and the greedy loop deletes it silently (#2703).
|
|
622
801
|
*
|
|
623
802
|
* Canonical home for this primitive (#2143 audit dedup): previously
|
|
624
803
|
* duplicated byte-identically in both `state.cts` and `state-transition.cts`.
|
|
625
804
|
*/
|
|
626
|
-
function stripFrontmatter(content) {
|
|
805
|
+
function stripFrontmatter(content, opts = {}) {
|
|
627
806
|
let result = content;
|
|
628
807
|
while (true) {
|
|
629
808
|
const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, '');
|
|
630
809
|
if (stripped === result)
|
|
631
810
|
break;
|
|
632
811
|
result = stripped;
|
|
812
|
+
if (opts.once)
|
|
813
|
+
break;
|
|
633
814
|
}
|
|
634
815
|
return result;
|
|
635
816
|
}
|
|
@@ -750,10 +931,19 @@ function cmdFrontmatterValidate(cwd, filePath, schemaName, raw) {
|
|
|
750
931
|
if (filePath.includes('\0')) {
|
|
751
932
|
error('file path contains null bytes');
|
|
752
933
|
}
|
|
753
|
-
|
|
754
|
-
|
|
934
|
+
// Guard against prototype-chain keys (__proto__, constructor, toString, hasOwnProperty,
|
|
935
|
+
// valueOf, ...): a bare FRONTMATTER_SCHEMAS[schemaName] lookup resolves those to
|
|
936
|
+
// Object.prototype members instead of undefined, so a `!schema` check on the raw
|
|
937
|
+
// lookup never fires and the code crashes later on `schema.required.filter` with an
|
|
938
|
+
// uncaught TypeError instead of the intended "Unknown schema" message. Confirmed live
|
|
939
|
+
// with --schema __proto__. Now that --schema is agent-bound (agents/gsd-planner.md's
|
|
940
|
+
// $SCHEMA), this is reachable from prompt state, not just an unreachable literal.
|
|
941
|
+
// Checked and rejected BEFORE the lookup (rather than `?? undefined`-ing the lookup
|
|
942
|
+
// itself) so `schema`'s inferred type stays non-optional and needs no assertion below.
|
|
943
|
+
if (!Object.prototype.hasOwnProperty.call(FRONTMATTER_SCHEMAS, schemaName)) {
|
|
755
944
|
error(`Unknown schema: ${schemaName}. Available: ${Object.keys(FRONTMATTER_SCHEMAS).join(', ')}`);
|
|
756
945
|
}
|
|
946
|
+
const schema = FRONTMATTER_SCHEMAS[schemaName];
|
|
757
947
|
const fullPath = node_path_1.default.isAbsolute(filePath) ? filePath : node_path_1.default.join(cwd, filePath);
|
|
758
948
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(fullPath);
|
|
759
949
|
if (!content) {
|
|
@@ -771,9 +961,25 @@ function cmdFrontmatterValidate(cwd, filePath, schemaName, raw) {
|
|
|
771
961
|
// Pass the resolved path so a truncated file is named in the diagnostic and deduplicated
|
|
772
962
|
// per file rather than per content digest (#1882, ADR-1411 wiring clause).
|
|
773
963
|
const fm = extractFrontmatter(content, fullPath);
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
964
|
+
const requiredValues = schema.requiredValues || {};
|
|
965
|
+
// A field satisfies the schema when it is present AND — for fields with a
|
|
966
|
+
// requiredValues entry — strictly equal to that value. Absent and
|
|
967
|
+
// wrong-value both surface as `missing` (existing `missing`/`present`
|
|
968
|
+
// partition of `required` is unchanged — no consumer reads `present` to
|
|
969
|
+
// mean "physically exists regardless of value," confirmed by searching
|
|
970
|
+
// every caller before choosing this). But #2847 review: folding silently
|
|
971
|
+
// made "missing" misleading for a WRONG-valued field the plan author can
|
|
972
|
+
// plainly see in the file (e.g. `gap_closure: True`) — nothing told them
|
|
973
|
+
// the field is present but the VALUE is wrong, so they could loop trying
|
|
974
|
+
// to add a field that is already there. `invalidValue` names exactly that
|
|
975
|
+
// subset (present, but not the required value) so the message stays
|
|
976
|
+
// actionable without changing what `missing`/`present` mean.
|
|
977
|
+
const wrongValue = (f) => fm[f] !== undefined && Object.prototype.hasOwnProperty.call(requiredValues, f) && fm[f] !== requiredValues[f];
|
|
978
|
+
const satisfies = (f) => fm[f] !== undefined && !wrongValue(f);
|
|
979
|
+
const missing = schema.required.filter(f => !satisfies(f));
|
|
980
|
+
const present = schema.required.filter(f => satisfies(f));
|
|
981
|
+
const invalidValue = schema.required.filter(wrongValue);
|
|
982
|
+
output({ valid: missing.length === 0, missing, present, invalidValue, schema: schemaName }, raw, missing.length === 0 ? 'valid' : 'invalid');
|
|
777
983
|
}
|
|
778
984
|
module.exports = {
|
|
779
985
|
extractFrontmatter,
|
|
@@ -793,4 +999,5 @@ module.exports = {
|
|
|
793
999
|
cmdFrontmatterSet,
|
|
794
1000
|
cmdFrontmatterMerge,
|
|
795
1001
|
cmdFrontmatterValidate,
|
|
1002
|
+
propagateCommentChannel,
|
|
796
1003
|
};
|
|
@@ -24,14 +24,18 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
24
24
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
25
25
|
const io = require("./io.cjs");
|
|
26
26
|
const { output, error } = io;
|
|
27
|
-
|
|
28
|
-
const phaseId = require("./phase-id.cjs");
|
|
29
|
-
const { escapeRegex } = phaseId;
|
|
27
|
+
const pattern_cjs_1 = require("./pattern.cjs");
|
|
30
28
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
29
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
32
30
|
const { planningPaths, planningDir, findContextMdIn } = planningWorkspace;
|
|
33
31
|
const decisions_cjs_1 = require("./decisions.cjs");
|
|
34
32
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
34
|
+
const planScanMod = require("./plan-scan.cjs");
|
|
35
|
+
const { scanPhasePlans } = planScanMod;
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
|
+
const phaseIdMod = require("./phase-id.cjs");
|
|
38
|
+
const { scopeToPhase } = phaseIdMod;
|
|
35
39
|
/**
|
|
36
40
|
* Parse REQ-IDs from REQUIREMENTS.md content.
|
|
37
41
|
*
|
|
@@ -91,7 +95,7 @@ function parseRequirements(reqMd) {
|
|
|
91
95
|
}
|
|
92
96
|
function detectCoverage(items, planText) {
|
|
93
97
|
return items.map(it => {
|
|
94
|
-
const re = new RegExp('\\b' + escapeRegex(it.id) + '\\b');
|
|
98
|
+
const re = new RegExp('\\b' + (0, pattern_cjs_1.escapeRegex)(it.id) + '\\b');
|
|
95
99
|
return {
|
|
96
100
|
source: it.source,
|
|
97
101
|
item: it.id,
|
|
@@ -152,6 +156,38 @@ const PHASE_REQ_RANGE_RE = /^(.+-)(\d+)\.\.(.+-)(\d+)$/;
|
|
|
152
156
|
* against pathological input like `X-1..X-100000` ballooning the comparison set.
|
|
153
157
|
*/
|
|
154
158
|
const MAX_PHASE_REQ_RANGE = 1000;
|
|
159
|
+
/**
|
|
160
|
+
* Shape filter applied to `--phase-req-ids` tokens AFTER range expansion
|
|
161
|
+
* (#3189). ROADMAP `**Requirements:**` lines routinely carry prose trailing
|
|
162
|
+
* the real ID list — locked-decision annotations, ambiguity scores,
|
|
163
|
+
* prohibitions, dates — and the function's own contract says callers may pass
|
|
164
|
+
* that roadmap value through verbatim. Without a shape filter every prose word
|
|
165
|
+
* survives the whitespace split and is reported as an individually-missing
|
|
166
|
+
* requirement, drowning the real coverage signal.
|
|
167
|
+
*
|
|
168
|
+
* The filter is intentionally wider than `ID_PATTERN` (which mandates a hyphen
|
|
169
|
+
* and so would reject the hyphen-less `R1`..`R8` family real roadmaps use).
|
|
170
|
+
* Three load-bearing details (#3189):
|
|
171
|
+
*
|
|
172
|
+
* 1. The digit lookahead `(?=.*\d)` matters. Without it ALL-CAPS prose tokens
|
|
173
|
+
* that appear in annotations (`LOCKED`, `TBD`-as-prose, `NONE`-as-prose)
|
|
174
|
+
* would pass and still reach the report as fake IDs. Every real
|
|
175
|
+
* requirement ID carries a digit (`R1`, `SEL-01`, `P1-P3`).
|
|
176
|
+
* 2. It MUST run after `expandPhaseReqIdToken`. Filtering before would
|
|
177
|
+
* discard the range tokens themselves (`SEL-01..SEL-03` matches no
|
|
178
|
+
* single-ID shape), silently undoing the #1269 / #1419 range expansion.
|
|
179
|
+
* 3. It is NOT reusable as `ID_PATTERN`. `ID_PATTERN` mandates a hyphen,
|
|
180
|
+
* so it rejects `R1`..`R8` and would leave only range-shaped tokens
|
|
181
|
+
* standing — a silently-empty report, which is worse.
|
|
182
|
+
*
|
|
183
|
+
* Accepts both hyphen-less digit-bearing IDs (`R1`, `R8`) and prefix-hyphen
|
|
184
|
+
* IDs (`REQ-01`, `SEL-01`, `BACK-07`), including multi-segment prefixes
|
|
185
|
+
* (`REQ2-01`). Rejects prose (`LOCKED`), punctuation (`—`, `##`, `+`),
|
|
186
|
+
* dates, version-likes (`0.12;`), backtick fragments, and any token still
|
|
187
|
+
* containing `.` (so invalid range tokens returned literal by
|
|
188
|
+
* `expandPhaseReqIdToken` are dropped rather than surfaced as fake IDs).
|
|
189
|
+
*/
|
|
190
|
+
const PHASE_REQ_ID_SHAPE_RE = /^(?=.*\d)[A-Z][A-Z0-9]*(?:[-_][A-Za-z0-9]+)*$/;
|
|
155
191
|
/**
|
|
156
192
|
* Expand a single `--phase-req-ids` token in place. If it is a valid ascending
|
|
157
193
|
* same-prefix numeric range (`<PREFIX>-NN..<PREFIX>-MM`, identical prefix both
|
|
@@ -212,6 +248,15 @@ function expandPhaseReqIdToken(token) {
|
|
|
212
248
|
* spanning more than MAX_PHASE_REQ_RANGE IDs) stays literal — no partial
|
|
213
249
|
* expansion, no guessing.
|
|
214
250
|
*
|
|
251
|
+
* ID-shape filter (#3189): STRICTLY AFTER range expansion, every token is
|
|
252
|
+
* filtered through `PHASE_REQ_ID_SHAPE_RE`. ROADMAP `**Requirements:**` lines
|
|
253
|
+
* routinely carry prose trailing the real ID list (locked-decision annotations,
|
|
254
|
+
* ambiguity scores, prohibitions, dates); without the filter every prose word
|
|
255
|
+
* would be reported as an individually-missing requirement. Tokens that cannot
|
|
256
|
+
* be requirement IDs (prose, punctuation, dates, invalid range tokens left
|
|
257
|
+
* literal by `expandPhaseReqIdToken`) are dropped; an input whose every token
|
|
258
|
+
* is dropped collapses to `null` (skip semantics).
|
|
259
|
+
*
|
|
215
260
|
* Tolerates JSON-array-ish input (`["REQ-01","REQ-02"]`) since callers may pass
|
|
216
261
|
* the roadmap value through verbatim.
|
|
217
262
|
*/
|
|
@@ -229,7 +274,12 @@ function normalizePhaseReqIds(rawVal) {
|
|
|
229
274
|
const ids = v.split(/[\s,]+/).map(s => s.trim()).filter(Boolean);
|
|
230
275
|
// Expand range tokens (#1269) per-token AFTER the split, preserving input order.
|
|
231
276
|
const expanded = ids.flatMap(expandPhaseReqIdToken);
|
|
232
|
-
|
|
277
|
+
// #3189: drop tokens that cannot be requirement IDs (prose, punctuation,
|
|
278
|
+
// dates, invalid range tokens left literal by expandPhaseReqIdToken). MUST
|
|
279
|
+
// run after expand so valid ranges still expand (filtering before would
|
|
280
|
+
// discard `SEL-01..SEL-03` itself, undoing #1269 / #1419).
|
|
281
|
+
const filtered = expanded.filter(t => PHASE_REQ_ID_SHAPE_RE.test(t));
|
|
282
|
+
return filtered.length === 0 ? null : filtered;
|
|
233
283
|
}
|
|
234
284
|
function runGapAnalysis(cwd, phaseDir, options = {}) {
|
|
235
285
|
const phaseReqIds = normalizePhaseReqIds(options.phaseReqIds);
|
|
@@ -268,7 +318,13 @@ function runGapAnalysis(cwd, phaseDir, options = {}) {
|
|
|
268
318
|
phaseDirFiles = node_fs_1.default.readdirSync(absPhaseDir);
|
|
269
319
|
}
|
|
270
320
|
catch { /* unreadable */ }
|
|
271
|
-
|
|
321
|
+
// #3511-class: scope the raw listing to this phase dir before the
|
|
322
|
+
// phase-numbered -CONTEXT.md predicate. `phaseDirFiles` itself stays raw —
|
|
323
|
+
// it is also reused below only as a `.length > 0` guard ahead of
|
|
324
|
+
// scanPhasePlans's own plan-sequence-numbered enumeration, a different
|
|
325
|
+
// grammar that must not be scoped by phase number.
|
|
326
|
+
const scopedPhaseDirFiles = scopeToPhase(phaseDirFiles, node_path_1.default.basename(absPhaseDir));
|
|
327
|
+
const ctxFile = findContextMdIn(scopedPhaseDirFiles);
|
|
272
328
|
const ctxPath = ctxFile ? node_path_1.default.join(absPhaseDir, ctxFile) : null;
|
|
273
329
|
const ctxMd = ctxPath ? node_fs_1.default.readFileSync(ctxPath, 'utf-8') : '';
|
|
274
330
|
// Use extractDecisions so gap-checker can distinguish could-not-parse from none-present.
|
|
@@ -278,7 +334,12 @@ function runGapAnalysis(cwd, phaseDir, options = {}) {
|
|
|
278
334
|
let planText = '';
|
|
279
335
|
try {
|
|
280
336
|
if (phaseDirFiles.length > 0) {
|
|
281
|
-
|
|
337
|
+
// #3183 (lint-plan-count-drift): source the live plan-file list from
|
|
338
|
+
// the single owner (scanPhasePlans) instead of a local `-PLAN\.md$`
|
|
339
|
+
// filter on the already-read listing — picks up bare PLAN.md, nested
|
|
340
|
+
// plans/, and excludes superseded plans, none of which the prior
|
|
341
|
+
// root-only exact-suffix filter did.
|
|
342
|
+
const files = scanPhasePlans(absPhaseDir).planFiles;
|
|
282
343
|
planText = files.map(f => {
|
|
283
344
|
try {
|
|
284
345
|
return node_fs_1.default.readFileSync(node_path_1.default.join(absPhaseDir, f), 'utf-8');
|
|
@@ -18,11 +18,10 @@
|
|
|
18
18
|
* non-zero check-command exit, which the workflow's two-step gate contract treats
|
|
19
19
|
* as a step-1 command failure (routed per the gate's `onError`).
|
|
20
20
|
*
|
|
21
|
-
* Built-in
|
|
22
|
-
* `sh -c` subprocess
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* for the full sandbox contract.
|
|
21
|
+
* Built-in kinds: `command-exit-zero` runs a declared command in a bounded
|
|
22
|
+
* `sh -c` subprocess; `artifact-frontmatter-equals` compares a declared value
|
|
23
|
+
* with frontmatter read through injected artifact dependencies. See ADR-2008
|
|
24
|
+
* for the full contracts.
|
|
26
25
|
*
|
|
27
26
|
* This is a leaf pure module: no fs, no child_process, no config — the subprocess
|
|
28
27
|
* seam is injected so the evaluator is trivially testable without spawning.
|
|
@@ -35,7 +34,7 @@ const COMMAND_MAX_OUTPUT_CHARS = 2000;
|
|
|
35
34
|
/** Hard cap on the declared command length (defense-in-depth against ARGV overflow / abuse). */
|
|
36
35
|
const COMMAND_MAX_LENGTH = 4096;
|
|
37
36
|
/** Predicate kinds this evaluator recognises (extensible — add to KIND_TABLE). */
|
|
38
|
-
const EVALUATOR_KINDS = Object.freeze(['command-exit-zero']);
|
|
37
|
+
const EVALUATOR_KINDS = Object.freeze(['command-exit-zero', 'artifact-frontmatter-equals']);
|
|
39
38
|
/** Placeholders interpolated into a declared command, in addition to sh's own vars. */
|
|
40
39
|
const INTERPOLATION_VAR_NAMES = Object.freeze(['PHASE_NUMBER', 'PHASE_DIR', 'PHASE_REQ_IDS']);
|
|
41
40
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -102,9 +101,53 @@ function evaluateCommandExitZero(predicate, ctx, deps) {
|
|
|
102
101
|
details: { kind: 'command-exit-zero', exitCode: res.exitCode, signal: res.signal },
|
|
103
102
|
};
|
|
104
103
|
}
|
|
104
|
+
// ─── Kind: artifact-frontmatter-equals ──────────────────────────────────────────
|
|
105
|
+
function evaluateArtifactFrontmatterEquals(predicate, ctx, deps) {
|
|
106
|
+
const artifactSuffix = predicate['artifact'];
|
|
107
|
+
if (!isNonEmptyString(artifactSuffix)) {
|
|
108
|
+
throw new Error('artifact-frontmatter-equals predicate requires a non-empty string "artifact"');
|
|
109
|
+
}
|
|
110
|
+
const field = predicate['field'];
|
|
111
|
+
if (!isNonEmptyString(field)) {
|
|
112
|
+
throw new Error('artifact-frontmatter-equals predicate requires a non-empty string "field"');
|
|
113
|
+
}
|
|
114
|
+
const expectedValue = predicate['equals'];
|
|
115
|
+
if (expectedValue === undefined) {
|
|
116
|
+
throw new Error('artifact-frontmatter-equals predicate requires an "equals" key');
|
|
117
|
+
}
|
|
118
|
+
const targetDir = isNonEmptyString(ctx.phaseDir) ? ctx.phaseDir : ctx.cwd;
|
|
119
|
+
const filePath = deps.findPhaseArtifact(targetDir, artifactSuffix);
|
|
120
|
+
if (!filePath) {
|
|
121
|
+
return {
|
|
122
|
+
block: true,
|
|
123
|
+
message: `Artifact matching ${artifactSuffix} not found in ${targetDir}`,
|
|
124
|
+
details: { kind: 'artifact-frontmatter-equals', artifactNotFound: true },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const fm = deps.readFrontmatter(filePath);
|
|
128
|
+
const actualValue = fm[field];
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
130
|
+
const expectedStr = typeof expectedValue === 'object' ? JSON.stringify(expectedValue) : String(expectedValue);
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
132
|
+
const actualStr = typeof actualValue === 'object' ? JSON.stringify(actualValue) : String(actualValue);
|
|
133
|
+
const matches = actualValue === expectedValue || (actualValue !== undefined && actualValue !== null && actualStr === expectedStr);
|
|
134
|
+
if (matches) {
|
|
135
|
+
return {
|
|
136
|
+
block: false,
|
|
137
|
+
message: `Frontmatter field "${field}" matches expected value (${expectedStr})`,
|
|
138
|
+
details: { kind: 'artifact-frontmatter-equals', match: true },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
block: true,
|
|
143
|
+
message: `Frontmatter field "${field}" in ${artifactSuffix} is ${actualStr}, expected ${expectedStr}`,
|
|
144
|
+
details: { kind: 'artifact-frontmatter-equals', match: false, actual: actualValue, expected: expectedValue },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
105
147
|
// ─── Kind dispatch table ──────────────────────────────────────────────────────
|
|
106
148
|
const KIND_TABLE = {
|
|
107
149
|
'command-exit-zero': evaluateCommandExitZero,
|
|
150
|
+
'artifact-frontmatter-equals': evaluateArtifactFrontmatterEquals,
|
|
108
151
|
};
|
|
109
152
|
// ─── Public entry point ───────────────────────────────────────────────────────
|
|
110
153
|
/**
|
|
@@ -133,6 +176,14 @@ function evaluatePredicate(predicate, context, deps) {
|
|
|
133
176
|
if (typeof kind !== 'string' || kind.length === 0) {
|
|
134
177
|
throw new Error('predicate.kind must be a non-empty string');
|
|
135
178
|
}
|
|
179
|
+
if (kind === 'artifact-frontmatter-equals') {
|
|
180
|
+
if (typeof d.findPhaseArtifact !== 'function') {
|
|
181
|
+
throw new Error('predicate deps require a "findPhaseArtifact" function');
|
|
182
|
+
}
|
|
183
|
+
if (typeof d.readFrontmatter !== 'function') {
|
|
184
|
+
throw new Error('predicate deps require a "readFrontmatter" function');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
136
187
|
const handler = KIND_TABLE[kind];
|
|
137
188
|
if (!handler) {
|
|
138
189
|
throw new Error(`Unknown predicate kind: "${kind}". Known kinds: ${EVALUATOR_KINDS.join(', ')}`);
|