@opengsd/gsd-core 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +12 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +22 -14
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +5 -3
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +23 -2
- package/agents/gsd-plan-checker.md +50 -53
- package/agents/gsd-planner.md +50 -50
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +2 -2
- package/agents/gsd-roadmapper.md +15 -11
- package/agents/gsd-ui-checker.md +63 -4
- package/agents/gsd-ui-researcher.md +41 -3
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +13 -4
- package/bin/install.js +1448 -1103
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/discuss-phase.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/import.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/quick.md +9 -5
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +1035 -138
- package/gsd-core/bin/lib/active-workstream-store.cjs +146 -22
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +392 -32
- package/gsd-core/bin/lib/api-coverage.cjs +33 -14
- package/gsd-core/bin/lib/artifacts.cjs +5 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +32 -15
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +1026 -268
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- 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-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +845 -130
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-state.cjs +18 -3
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +700 -40
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +216 -42
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +735 -0
- package/gsd-core/bin/lib/command-aliases.cjs +22 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +1172 -108
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1192 -0
- package/gsd-core/bin/lib/config-loader.cjs +187 -23
- package/gsd-core/bin/lib/config.cjs +102 -3
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +208 -33
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/edge-probe.cjs +9 -1
- package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
- package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +899 -229
- package/gsd-core/bin/lib/gap-checker.cjs +95 -10
- package/gsd-core/bin/lib/git-base-branch.cjs +276 -39
- package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +149 -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 +268 -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 +187 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +451 -0
- package/gsd-core/bin/lib/host-integration.cjs +39 -6
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init-command-router.cjs +118 -21
- package/gsd-core/bin/lib/init.cjs +439 -168
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +811 -259
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +235 -0
- package/gsd-core/bin/lib/install-profiles.cjs +212 -61
- 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-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +148 -38
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +170 -15
- package/gsd-core/bin/lib/learnings.cjs +85 -14
- package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +183 -22
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +842 -73
- package/gsd-core/bin/lib/model-catalog.cjs +232 -16
- package/gsd-core/bin/lib/model-resolver.cjs +193 -68
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +18 -9
- package/gsd-core/bin/lib/phase-id.cjs +514 -40
- package/gsd-core/bin/lib/phase-lifecycle.cjs +52 -19
- package/gsd-core/bin/lib/phase-locator.cjs +262 -34
- package/gsd-core/bin/lib/phase.cjs +1038 -214
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- package/gsd-core/bin/lib/plan-document.cjs +263 -0
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +98 -3
- package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +894 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +112 -6
- package/gsd-core/bin/lib/probe-core.cjs +5 -2
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
- package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
- package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +766 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -13
- package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
- package/gsd-core/bin/lib/roadmap-command-router.cjs +59 -11
- package/gsd-core/bin/lib/roadmap-parser.cjs +1006 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +442 -96
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +702 -52
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +459 -55
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +402 -58
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +96 -8
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +342 -7
- package/gsd-core/bin/lib/smart-entry.cjs +133 -23
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +52 -19
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +338 -8
- package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
- package/gsd-core/bin/lib/state-transition.cjs +846 -176
- package/gsd-core/bin/lib/state.cjs +2589 -369
- package/gsd-core/bin/lib/surface.cjs +33 -11
- package/gsd-core/bin/lib/task-command-router.cjs +111 -1
- package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
- package/gsd-core/bin/lib/teams-status.cjs +4 -1
- 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 +67 -23
- package/gsd-core/bin/lib/uat.cjs +1761 -167
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +51 -12
- package/gsd-core/bin/lib/unusable-input.cjs +37 -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-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +75 -0
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -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 +272 -9
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify.cjs +453 -918
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
- package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/workstream.cjs +2 -2
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +341 -18
- package/gsd-core/bin/shared/config-defaults.manifest.json +8 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +12 -1
- package/gsd-core/bin/shared/exit-codes.json +8 -0
- package/gsd-core/bin/shared/exit-codes.sh +20 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -1
- package/gsd-core/references/agent-contracts.md +44 -26
- package/gsd-core/references/api-coverage.md +24 -2
- package/gsd-core/references/autonomous-smart-discuss.md +3 -3
- package/gsd-core/references/checkpoints.md +39 -21
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/edge-probe.md +8 -0
- package/gsd-core/references/execute-mvp-tdd.md +4 -6
- package/gsd-core/references/execute-phase-between-wave-reset.md +15 -14
- 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 +17 -11
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-integration.md +5 -5
- package/gsd-core/references/git-planning-commit.md +5 -4
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +61 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/nyquist-compliance.md +74 -0
- package/gsd-core/references/offer-next.md +3 -5
- package/gsd-core/references/phase-argument-parsing.md +3 -3
- package/gsd-core/references/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planner-revision.md +1 -1
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +44 -13
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +1 -1
- package/gsd-core/references/tdd.md +1 -3
- package/gsd-core/references/ui-brand.md +65 -21
- package/gsd-core/references/ui-consideration-probe.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +5 -5
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +2 -2
- package/gsd-core/references/workstream-flag.md +33 -17
- package/gsd-core/templates/README.md +1 -1
- package/gsd-core/templates/SECURITY.md +3 -3
- package/gsd-core/templates/UI-SPEC.md +25 -3
- package/gsd-core/templates/VALIDATION.md +3 -3
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +5 -4
- package/gsd-core/templates/state.md +11 -4
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +3 -3
- package/gsd-core/workflows/add-tests.md +3 -8
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +13 -20
- package/gsd-core/workflows/audit-fix.md +12 -3
- package/gsd-core/workflows/audit-milestone.md +9 -9
- package/gsd-core/workflows/audit-uat.md +17 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +11 -27
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +64 -5
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +14 -4
- package/gsd-core/workflows/code-review-fix.md +38 -11
- package/gsd-core/workflows/code-review.md +159 -52
- package/gsd-core/workflows/complete-milestone.md +151 -23
- package/gsd-core/workflows/debug.md +12 -8
- package/gsd-core/workflows/diagnose-issues.md +47 -15
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -8
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +3 -6
- package/gsd-core/workflows/docs-update.md +5 -4
- package/gsd-core/workflows/edit-phase.md +27 -2
- package/gsd-core/workflows/eval-review.md +7 -14
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +142 -15
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
- 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 +24 -4
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase.md +72 -100
- package/gsd-core/workflows/execute-plan.md +52 -15
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +5 -5
- package/gsd-core/workflows/health.md +76 -10
- package/gsd-core/workflows/import.md +18 -15
- package/gsd-core/workflows/inbox.md +4 -5
- package/gsd-core/workflows/ingest-docs.md +49 -16
- package/gsd-core/workflows/insert-phase.md +5 -5
- package/gsd-core/workflows/list-seeds.md +5 -3
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +12 -23
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +8 -5
- package/gsd-core/workflows/new-milestone.md +22 -29
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +26 -40
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +14 -2
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
- package/gsd-core/workflows/plan-phase.md +162 -59
- package/gsd-core/workflows/plan-review-convergence.md +96 -11
- package/gsd-core/workflows/plant-seed.md +2 -2
- package/gsd-core/workflows/pr-branch.md +187 -51
- package/gsd-core/workflows/profile-user.md +16 -14
- package/gsd-core/workflows/progress.md +61 -18
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +5 -7
- package/gsd-core/workflows/quick/steps/quick-verification.md +28 -9
- package/gsd-core/workflows/quick/steps/research-phase.md +4 -6
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick.md +55 -44
- package/gsd-core/workflows/remove-phase.md +4 -4
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +8 -12
- package/gsd-core/workflows/review.md +219 -20
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings-advanced.md +7 -9
- package/gsd-core/workflows/settings-integrations.md +64 -31
- package/gsd-core/workflows/settings.md +69 -7
- package/gsd-core/workflows/ship.md +116 -50
- package/gsd-core/workflows/sketch-wrap-up.md +11 -17
- package/gsd-core/workflows/sketch.md +12 -18
- package/gsd-core/workflows/smart-entry.md +3 -5
- package/gsd-core/workflows/spec-phase.md +53 -13
- package/gsd-core/workflows/spike-wrap-up.md +7 -11
- package/gsd-core/workflows/spike.md +20 -31
- package/gsd-core/workflows/stats.md +2 -2
- package/gsd-core/workflows/sync-skills.md +64 -9
- package/gsd-core/workflows/thread.md +11 -7
- package/gsd-core/workflows/transition.md +49 -14
- package/gsd-core/workflows/ui-phase.md +15 -21
- package/gsd-core/workflows/ui-review.md +8 -12
- package/gsd-core/workflows/ultraplan-phase.md +5 -13
- package/gsd-core/workflows/undo.md +8 -16
- package/gsd-core/workflows/update.md +7 -11
- package/gsd-core/workflows/validate-phase.md +3 -3
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +66 -25
- package/hooks/dist/gsd-agent-isolation-guard.js +158 -30
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +19 -10
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +2 -3
- package/hooks/dist/gsd-cursor-session-start.js +2 -1
- package/hooks/dist/gsd-cursor-stop.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +83 -3
- package/hooks/dist/gsd-cursor-subagent-stop.js +6 -3
- package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
- package/hooks/dist/gsd-graphify-update.sh +22 -18
- package/hooks/dist/gsd-node-runner.sh +76 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +37 -27
- package/hooks/dist/gsd-read-guard.js +16 -7
- package/hooks/dist/gsd-read-injection-scanner.js +55 -32
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +231 -24
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-validate-commit.sh +80 -6
- package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
- package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
- package/hooks/dist/gsd-workflow-guard.js +162 -46
- package/hooks/dist/gsd-worktree-path-guard.js +36 -21
- package/hooks/dist/gsd-write-guard.js +35 -25
- package/hooks/dist/lib/cli-exit.js +560 -0
- package/hooks/dist/lib/exit-code-registry.js +98 -0
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/git-probe.js +84 -0
- package/hooks/dist/lib/hook-exit.js +81 -0
- package/hooks/dist/lib/injection-patterns.js +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +9 -0
- package/hooks/dist/managed-hooks-registry.cjs +3 -0
- package/hooks/gsd-agent-isolation-guard.js +158 -30
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +19 -10
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +2 -3
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +83 -3
- package/hooks/gsd-cursor-subagent-stop.js +6 -3
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +76 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +37 -27
- package/hooks/gsd-read-guard.js +16 -7
- package/hooks/gsd-read-injection-scanner.js +55 -32
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +231 -24
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-validate-commit.sh +80 -6
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +162 -46
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +9 -0
- package/hooks/managed-hooks-registry.cjs +3 -0
- package/package.json +28 -11
- package/pi/gsd.cjs +19 -5
- package/scripts/base64-scan.sh +74 -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 +5 -0
- package/scripts/changeset/lint.cjs +60 -5
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/check-glossary-refs.cjs +77 -15
- package/scripts/check-mutation-score-ratchet.cjs +156 -0
- package/scripts/ci-check-job-near-cap.cjs +49 -0
- package/scripts/ci-pr-mergeability.cjs +262 -0
- package/scripts/ci-test-scope.cjs +64 -14
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/docs-guard-registry.cjs +396 -0
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-registry.cjs +11 -21
- package/scripts/gen-context-index.cjs +2 -11
- package/scripts/gen-exit-code-docs.cjs +318 -0
- package/scripts/gen-exit-code-registry.cjs +891 -0
- package/scripts/gen-features.cjs +836 -0
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +138 -25
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/gen-scripts-cli-exit.cjs +185 -0
- package/scripts/gen-state-md-docs.cjs +727 -0
- package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +308 -0
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
- package/scripts/lint-canary-version-leak.cjs +73 -0
- package/scripts/lint-command-contract.cjs +96 -13
- package/scripts/lint-completion-predicate-drift.cjs +933 -0
- package/scripts/lint-completion-ratio-drift.cjs +214 -0
- package/scripts/lint-default-flip-documentation.cjs +193 -0
- package/scripts/lint-docs-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +38 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +461 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +492 -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 +471 -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 +488 -0
- package/scripts/lint-seam-enforcement.cjs +182 -0
- package/scripts/lint-slug-derivation-drift.cjs +921 -0
- package/scripts/lint-source-test-name-collision.cjs +241 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +950 -0
- package/scripts/lint-test-file-count.allowlist.json +137 -8
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +830 -0
- package/scripts/lint-vendored-deps.cjs +297 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +100 -14
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +24 -7
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-next/SKILL.md +0 -1
- package/skills/gsd-plan-phase/SKILL.md +0 -1
- package/skills/gsd-progress/SKILL.md +0 -1
- package/skills/gsd-quick/SKILL.md +9 -5
- 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/bin/lib/ui-safety-gate.cjs +0 -107
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Anti-divergence drift guard for the STATE.md WRITE PATH — epic #3408, issue
|
|
6
|
+
* #3468, ADR-3408 Decision 5, contract §8.1/§8.2/§8.3
|
|
7
|
+
* (`docs/adr/3408-state-write-path-preservation.md` is the contract this
|
|
8
|
+
* guard enforces; read it first) — SHRUNK per ADR-3473 §8.6 (issue #3871).
|
|
9
|
+
*
|
|
10
|
+
* WHAT MOVED INTO THE TYPE SYSTEM (ADR-3473 §8.6): `writeStateMd`'s third
|
|
11
|
+
* parameter now REQUIRES a `StateTransaction` of `kind: 'rebuild'`, produced
|
|
12
|
+
* only by `openStateTransaction()` / `rebuildStateTransaction()`
|
|
13
|
+
* (`src/state-transition.cts`) — `writeStateMd` itself throws
|
|
14
|
+
* `STATE_TRANSACTION_KIND_INVALID` for anything else. The two exceptions this
|
|
15
|
+
* guard used to track as a RATCHETED STRING MATCH against
|
|
16
|
+
* `scripts/state-write-path-drift-baseline.json` — `cmdStateSync`
|
|
17
|
+
* (`src/state.cts`, #905's "let the body win") and `REGENERATE_STATE`
|
|
18
|
+
* (`src/health-diagnostic.cts`, `/gsd-health --repair`'s factory reset) — are
|
|
19
|
+
* NO LONGER TRACKED HERE. Both are now a constructor the type system names
|
|
20
|
+
* (`rebuildStateTransaction`), not an entry a human had to remember to keep
|
|
21
|
+
* acknowledging in a baseline file. That baseline file, and the whole
|
|
22
|
+
* ratchet machinery that existed ONLY to support it (loading it, the
|
|
23
|
+
* STALE-entry check, `--baseline` regeneration), is retired along with it —
|
|
24
|
+
* the `writeStateMd(` ARM of the old `findSeamBypasses` axis is gone for
|
|
25
|
+
* good, because the type system now names both of its exceptions.
|
|
26
|
+
*
|
|
27
|
+
* WHAT STAYED, RETITLED, AND MADE TERMINAL (issue #3871 review): the OTHER
|
|
28
|
+
* half of `findSeamBypasses` — every direct `syncStateFrontmatter(` /
|
|
29
|
+
* `applyPostSyncPreservation(` call outside their owner
|
|
30
|
+
* (`syncAndPreserveStateMd`, `src/state.cts`) — is NOT redundant. The type
|
|
31
|
+
* system gates `writeStateMd`'s THIRD PARAMETER; it says nothing about a call
|
|
32
|
+
* site that re-assembles `syncStateFrontmatter` + `applyPostSyncPreservation`
|
|
33
|
+
* itself instead of calling the one write-seam composition. ADR-3408 §8.3:
|
|
34
|
+
* "Assembling the stages at a call site is a re-derivation even when every
|
|
35
|
+
* step calls the owner." #3469 found exactly that shape live in
|
|
36
|
+
* `cmdPhaseComplete`'s atomic-commit adapter (`src/phase.cts`) — every step
|
|
37
|
+
* called an owner, so an owner-level test and this guard's OLD, narrower
|
|
38
|
+
* scan both stayed green while the composition itself drifted from
|
|
39
|
+
* `readModifyWriteStateMd`'s. See `findCompositionBypasses` below. Unlike
|
|
40
|
+
* the retired `writeStateMd(` arm, this one ships TERMINAL, not ratcheted —
|
|
41
|
+
* mirrors `findPromptSeamUses`'s own conversion in this same shrink: no
|
|
42
|
+
* legitimate call site outside the owner exists today, so any occurrence is
|
|
43
|
+
* a violation, not an entry to acknowledge.
|
|
44
|
+
*
|
|
45
|
+
* WHAT THIS GUARD STILL OWNS, because the type system cannot make it
|
|
46
|
+
* unrepresentable:
|
|
47
|
+
*
|
|
48
|
+
* AXIS 1 — POLICY DISPATCH (§8.1). `applyStatePreservation` must select
|
|
49
|
+
* its branch from a `FIELD_CLASSIFICATION` row's `preservation` value,
|
|
50
|
+
* never from a field NAME. Every `getFieldClassification('<string
|
|
51
|
+
* literal>')` inside `src/state-transition.cts` is a field-name-keyed
|
|
52
|
+
* branch — the shape that let four declared rows go unimplemented until
|
|
53
|
+
* #3258, and that leaves `derive` and `clear` with no executor today. A
|
|
54
|
+
* VARIABLE argument (`getFieldClassification(field)`) is the CORRECT
|
|
55
|
+
* table-driven shape and is deliberately NOT matched. Also matched: a
|
|
56
|
+
* direct `field === '<literal>'` / `field !== '<literal>'` / `'<literal>'
|
|
57
|
+
* === field` comparison of the dispatch loop's own `field` variable — the
|
|
58
|
+
* same prohibited shape routed AROUND `getFieldClassification` instead of
|
|
59
|
+
* through it (#3468 found this exact form live in `applyPreserveIfPlaceholder`,
|
|
60
|
+
* undetected by the call-shape check alone). Scoped to the identifier
|
|
61
|
+
* `field` only; see `FIELD_VAR_EQ_LITERAL_RE`'s own comment for why.
|
|
62
|
+
*
|
|
63
|
+
* AXIS 2 — RAW STATE WRITE (§8.6, RETAINED). A direct `fs.writeFileSync(`
|
|
64
|
+
* call whose target argument is the state path (`statePath`, or a literal
|
|
65
|
+
* containing `STATE.md`) is a write that skips BOTH the write seam
|
|
66
|
+
* (`writeStateMd`/`syncAndPreserveStateMd`) AND the OS Shell Projection
|
|
67
|
+
* seam (`platformWriteSync`, `src/shell-command-projection.cts`) entirely.
|
|
68
|
+
* No constructor or type can make this unrepresentable — Node's `fs`
|
|
69
|
+
* module is always one `import` away — so this axis stays a plain
|
|
70
|
+
* string-match scan, unratcheted: any occurrence is a violation, because no
|
|
71
|
+
* legitimate call site in this codebase writes STATE.md this way (every
|
|
72
|
+
* real writer goes through `platformWriteSync`).
|
|
73
|
+
*
|
|
74
|
+
* AXIS 3 — FRONTMATTER-SHAPED WRITE (§8.3(b), closed Phase 2 / #3469).
|
|
75
|
+
* `findUnstrippedContentWrites` below flags a `stateReplaceField(` call
|
|
76
|
+
* only when BOTH (a) its field-name argument is a VARIABLE, not a fixed
|
|
77
|
+
* string literal, and (b) its content argument has not been run through
|
|
78
|
+
* `stripFrontmatter` first (a narrow backward-scan approximation, not full
|
|
79
|
+
* dataflow — see the function's own docstring).
|
|
80
|
+
*
|
|
81
|
+
* AXIS 4 — PROMPT-LAYER WRITE (§8.3, Decision 4(d)). Prose in the prompt
|
|
82
|
+
* layer instructing an agent to shell out to a write-side `gsd-tools`
|
|
83
|
+
* subcommand is the same write seam, expressed as markdown rather than
|
|
84
|
+
* TypeScript — a check the type system cannot reach at all, since markdown
|
|
85
|
+
* is never compiled. Any occurrence is a violation.
|
|
86
|
+
*
|
|
87
|
+
* AXIS 5 — COMPOSITION BYPASS (§8.3, RETAINED, issue #3871). A direct
|
|
88
|
+
* `syncStateFrontmatter(` or `applyPostSyncPreservation(` call outside
|
|
89
|
+
* their owner (`syncAndPreserveStateMd`) is a re-assembly of the write-seam
|
|
90
|
+
* composition — the exact shape #3469 found live in `cmdPhaseComplete`.
|
|
91
|
+
* `writeStateMd(` is deliberately NOT scanned here (that arm is retired,
|
|
92
|
+
* §8.6) — `writeStateMd`'s own legitimate direct `syncStateFrontmatter(`
|
|
93
|
+
* call (the sanctioned #905 exception) is instead exempted by function
|
|
94
|
+
* name, same as the composition owner itself; see
|
|
95
|
+
* `SEAM_OWNER_EXEMPT_FUNCTIONS`. Terminal: any occurrence is a violation.
|
|
96
|
+
*
|
|
97
|
+
* DESIGN CONSTRAINTS (ADR-3180 Decision 4, adopted verbatim by ADR-3408):
|
|
98
|
+
* - 4(a) whole-repo scan, never an allowlist. ADR-3180's own phases found
|
|
99
|
+
* 26/5/54 copies where their epics scoped 3/3/4 — a scoped guard earns
|
|
100
|
+
* nothing.
|
|
101
|
+
* - 4(d) the scan surface is DECLARED and is NOT just `src/` — `src/`
|
|
102
|
+
* alone is itself an allowlist one directory wide; #1762 traced a wrong
|
|
103
|
+
* count to a shell snippet in `gsd-core/workflows/progress.md`.
|
|
104
|
+
*
|
|
105
|
+
* GOODHART, PER ADR-3408 DECISION 5: "0 violations" is a LAGGING metric — a
|
|
106
|
+
* measure about to become a target. This guard's own `_comment` and its
|
|
107
|
+
* human-readable success message both say so: the zero this guard reports
|
|
108
|
+
* must NEVER be quoted alone; it is only meaningful beside the behavioral
|
|
109
|
+
* identity test's result (the consumer-output assertion Decision 5's gaming
|
|
110
|
+
* table names as the actual defense).
|
|
111
|
+
*
|
|
112
|
+
* String literals are matched, never parsed as an AST — deliberately, per
|
|
113
|
+
* `scripts/lint-state-field-drift.cjs`'s own precedent: over-reporting
|
|
114
|
+
* (flagging a comment or a string that merely looks like a call) is safe;
|
|
115
|
+
* under-reporting (missing a real bypass) is the failure this guard exists
|
|
116
|
+
* to prevent. `stripComments` does not track quoted strings for exactly
|
|
117
|
+
* this reason — see its own header.
|
|
118
|
+
*
|
|
119
|
+
* CORRECTION TO ADR-3473 §8.6's TEXT, RECORDED HERE SO A FUTURE READER
|
|
120
|
+
* COMPARING THE ADR TO THIS FILE DOES NOT CONCLUDE THE FILE DRIFTED:
|
|
121
|
+
* §8.6 says this guard "keeps only its raw-write check (`fs.writeFileSync`
|
|
122
|
+
* against the state path), which the type cannot make unrepresentable." That
|
|
123
|
+
* sentence is wrong on both halves. First, no such check existed anywhere in
|
|
124
|
+
* this file before this shrink — `findRawStateWrites` (Axis 2, below) is
|
|
125
|
+
* NET-NEW, written for this shrink, not retained from a prior version.
|
|
126
|
+
* Second, this file did not (and does not) drop to "only" one check: besides
|
|
127
|
+
* the retired `writeStateMd(` arm of the old `findSeamBypasses` axis (the
|
|
128
|
+
* half §8.6 correctly names for removal, since the type system now names
|
|
129
|
+
* both of its exceptions), `findPolicyDispatchDrift`, `findUnimplementedPolicies`,
|
|
130
|
+
* `findUnstrippedContentWrites`, `findPromptSeamUses`, and the RETAINED
|
|
131
|
+
* `syncStateFrontmatter`/`applyPostSyncPreservation` composition-bypass half
|
|
132
|
+
* of `findSeamBypasses` (now `findCompositionBypasses`, terminal — issue
|
|
133
|
+
* #3871 review) all remain, because §8.6 names neither them nor anything
|
|
134
|
+
* that makes what they check unrepresentable — a field-name-keyed dispatch
|
|
135
|
+
* branch, an unimplemented `FieldPreservation` policy, an unstripped
|
|
136
|
+
* frontmatter write, prompt-layer prose shelling out to `gsd-tools`, and a
|
|
137
|
+
* re-assembled write-seam composition are all still exactly as representable
|
|
138
|
+
* in TypeScript (or in markdown, for the prompt-layer one) after the
|
|
139
|
+
* state-transaction constructor as they were before it — the constructor
|
|
140
|
+
* gates `writeStateMd`'s third parameter, nothing about a call site that
|
|
141
|
+
* never goes through `writeStateMd` at all. Do not edit the ADR to match
|
|
142
|
+
* this file; this paragraph is the correction of record.
|
|
143
|
+
*/
|
|
144
|
+
|
|
145
|
+
const path = require('node:path');
|
|
146
|
+
const { scanTree, sanitizeForReport } = require('./lib/drift-scan.cjs');
|
|
147
|
+
const { escapeRegex } = require('../gsd-core/bin/lib/pattern.cjs');
|
|
148
|
+
|
|
149
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
150
|
+
|
|
151
|
+
// Frozen REASON enum — mirrors `lint-state-field-drift.cjs`'s house style of
|
|
152
|
+
// naming every failure shape explicitly rather than reusing one generic
|
|
153
|
+
// "violation" string, so a reader can `grep` a reason string straight back
|
|
154
|
+
// to the paragraph of this header (or of the ADR) that explains it.
|
|
155
|
+
const REASON = Object.freeze({
|
|
156
|
+
FIELD_NAME_DISPATCH: 'field_name_dispatch',
|
|
157
|
+
UNIMPLEMENTED_POLICY: 'unimplemented_policy',
|
|
158
|
+
// Axis 3 (§8.3(b), closed Phase 2 / #3469): a `stateReplaceField(` call
|
|
159
|
+
// with a variable field-name argument whose content argument was not run
|
|
160
|
+
// through `stripFrontmatter` first — see `findUnstrippedContentWrites`.
|
|
161
|
+
UNSTRIPPED_CONTENT_WRITE: 'unstripped_content_write',
|
|
162
|
+
// Axis 2 (§8.6, retained): a raw `fs.writeFileSync(` call targeting the
|
|
163
|
+
// state path — see `findRawStateWrites`.
|
|
164
|
+
RAW_STATE_WRITE: 'raw_state_write',
|
|
165
|
+
// Axis 4 (§8.3, Decision 4(d)): prompt-layer prose shelling out to a
|
|
166
|
+
// write-side `gsd-tools` subcommand — see `findPromptSeamUses`.
|
|
167
|
+
PROMPT_LAYER_STATE_WRITE: 'prompt_layer_state_write',
|
|
168
|
+
// Axis 5 (§8.3, RETAINED, issue #3871): a direct `syncStateFrontmatter(` or
|
|
169
|
+
// `applyPostSyncPreservation(` call outside their owner
|
|
170
|
+
// (`syncAndPreserveStateMd`) — see `findCompositionBypasses`.
|
|
171
|
+
COMPOSITION_BYPASS: 'composition_bypass',
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Scan surface — declared, per Decision 4(d), never inferred from `src/`
|
|
175
|
+
// alone. `src/` covers the executor; the prompt layer covers markdown that
|
|
176
|
+
// can shell out to `state.patch` / `phase.complete` and post-process the
|
|
177
|
+
// result outside any TypeScript this guard could see.
|
|
178
|
+
const SRC_DIRS = ['src'];
|
|
179
|
+
const SRC_EXT = new Set(['.cts']);
|
|
180
|
+
const PROMPT_DIRS = ['gsd-core/workflows', 'commands', 'agents', 'skills'];
|
|
181
|
+
const PROMPT_EXT = new Set(['.md']);
|
|
182
|
+
|
|
183
|
+
// The executor (Axis 1 / Axis 3). Forward-slash literal: every `rel` this
|
|
184
|
+
// guard compares against it is unconditionally POSIX-normalized first
|
|
185
|
+
// (`toPosixRel` below) — never gated on `process.platform`.
|
|
186
|
+
const EXECUTOR_FILE = 'src/state-transition.cts';
|
|
187
|
+
|
|
188
|
+
// The write-seam composition owner (Axis 5). Forward-slash literal, same
|
|
189
|
+
// POSIX-normalization rule as `EXECUTOR_FILE` above.
|
|
190
|
+
const SEAM_OWNER_FILE = 'src/state.cts';
|
|
191
|
+
|
|
192
|
+
// Per Decision 4(d)'s "owner FILE is not exempt, only its named canonical
|
|
193
|
+
// FUNCTIONS are": a `syncStateFrontmatter(`/`applyPostSyncPreservation(` call
|
|
194
|
+
// inside one of these two functions, in `SEAM_OWNER_FILE` only, is the
|
|
195
|
+
// seam's own internal plumbing, not a bypass. `writeStateMd` is the
|
|
196
|
+
// `cmdStateSync`/`REGENERATE_STATE` path's own I/O wrapper calling
|
|
197
|
+
// `syncStateFrontmatter` directly (no preservation, by design — §8.3's
|
|
198
|
+
// closed exception list; ADR-3473 §8.6 gates ITS third parameter, which is
|
|
199
|
+
// an orthogonal, type-level check — this guard's exemption is about which
|
|
200
|
+
// FUNCTION BODY a raw call to the two seam stages is allowed to live in).
|
|
201
|
+
// `syncAndPreserveStateMd` is the ONE write-seam composition — every OTHER
|
|
202
|
+
// caller needing a non-standard I/O envelope routes through it. Every OTHER
|
|
203
|
+
// function in `state.cts` — and every function in every OTHER file — is
|
|
204
|
+
// still scanned and still flagged; in particular `readModifyWriteStateMd` is
|
|
205
|
+
// NOT exempt: it calls `syncAndPreserveStateMd` like everyone else, so if a
|
|
206
|
+
// direct `syncStateFrontmatter(`/`applyPostSyncPreservation(` call
|
|
207
|
+
// reappeared there it would be exactly the re-assembly shape this axis
|
|
208
|
+
// exists to catch.
|
|
209
|
+
const SEAM_OWNER_EXEMPT_FUNCTIONS = ['writeStateMd', 'syncAndPreserveStateMd'];
|
|
210
|
+
|
|
211
|
+
// Unconditional path-separator normalization (never gated on
|
|
212
|
+
// `process.platform` — a Windows-authored fork PR must be judged by the
|
|
213
|
+
// same POSIX-relative rule as everything else this guard reads).
|
|
214
|
+
function toPosixRel(rel) {
|
|
215
|
+
return rel.split(path.sep).join('/');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Strip `//` line comments and `/* ... *\/` block comments from `text`,
|
|
220
|
+
* returning one entry PER INPUT LINE so line numbers computed against the
|
|
221
|
+
* result stay correct against the original file. Block comments are tracked
|
|
222
|
+
* across lines (`inBlock`); line comments only ever affect their own line.
|
|
223
|
+
*
|
|
224
|
+
* Deliberately does NOT parse string/template literal contents — a `//` or
|
|
225
|
+
* `/*` embedded inside a quoted string is treated exactly like real source,
|
|
226
|
+
* which can occasionally UNDER-strip (leaving a would-be-comment's text
|
|
227
|
+
* live) but never OVER-strips real code into invisibility. Per this guard's
|
|
228
|
+
* header and `lint-state-field-drift.cjs`'s own precedent: over-reporting a
|
|
229
|
+
* documentation paragraph that merely DESCRIBES a call (ADR-3180 Amendment
|
|
230
|
+
* 3's exact false positive) is the failure this exists to prevent; a rare
|
|
231
|
+
* miss on an adversarial one-line string is an accepted, narrower risk in
|
|
232
|
+
* the opposite (safe) direction — under-reporting, never over-reporting.
|
|
233
|
+
*/
|
|
234
|
+
function stripComments(text) {
|
|
235
|
+
const lines = text.split('\n');
|
|
236
|
+
const out = new Array(lines.length);
|
|
237
|
+
let inBlock = false;
|
|
238
|
+
for (let i = 0; i < lines.length; i++) {
|
|
239
|
+
const line = lines[i];
|
|
240
|
+
let result = '';
|
|
241
|
+
let j = 0;
|
|
242
|
+
while (j < line.length) {
|
|
243
|
+
if (inBlock) {
|
|
244
|
+
const close = line.indexOf('*/', j);
|
|
245
|
+
if (close === -1) {
|
|
246
|
+
j = line.length;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
j = close + 2;
|
|
250
|
+
inBlock = false;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (line[j] === '/' && line[j + 1] === '/') {
|
|
254
|
+
j = line.length; // rest of line is a line comment
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
if (line[j] === '/' && line[j + 1] === '*') {
|
|
258
|
+
inBlock = true;
|
|
259
|
+
j += 2;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
result += line[j];
|
|
263
|
+
j++;
|
|
264
|
+
}
|
|
265
|
+
out[i] = result;
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// A named function declaration, tolerating `export`/`async` prefixes — the
|
|
271
|
+
// SAME shape `nearestPrecedingAssignment` uses as its backward-scan boundary,
|
|
272
|
+
// and `enclosingFunction` below uses to recognise (and skip) the seam
|
|
273
|
+
// functions' own definitions.
|
|
274
|
+
const FUNCTION_DECL_LINE_RE = /^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Nearest preceding named-function declaration, scanning `lines` BACKWARD
|
|
278
|
+
* from `index`. Scopes an exemption to a FUNCTION, never a FILE — a whole-
|
|
279
|
+
* file owner exemption is precisely how `getMilestoneInfo` stayed invisible
|
|
280
|
+
* to an earlier drift guard (ADR-3180 Decision 4(d)'s own cautionary case,
|
|
281
|
+
* cross-referenced by this guard's header).
|
|
282
|
+
*/
|
|
283
|
+
function enclosingFunction(lines, index) {
|
|
284
|
+
for (let i = index; i >= 0; i--) {
|
|
285
|
+
const m = FUNCTION_DECL_LINE_RE.exec(lines[i]);
|
|
286
|
+
if (m) return m[1];
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// One member of the `FieldPreservation` union, e.g. `'preserve-always'` —
|
|
292
|
+
// lowercase-with-dashes, single-quoted.
|
|
293
|
+
const POLICY_UNION_START_RE = /export\s+type\s+FieldPreservation\s*=/;
|
|
294
|
+
const POLICY_UNION_MEMBER_RE = /'([a-z][a-z-]*)'/g;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Parse the members of `export type FieldPreservation = 'a' | 'b' | ...;`
|
|
298
|
+
* straight out of the executor's own source, so this guard cannot drift
|
|
299
|
+
* from the type it polices (a hardcoded copy of the union would be exactly
|
|
300
|
+
* the "declared here, enforced somewhere else" shape ADR-3408 exists to
|
|
301
|
+
* remove — this time inside the GUARD). In `src/state-transition.cts` the
|
|
302
|
+
* union is declared across several lines, each shaped like
|
|
303
|
+
* ` | 'clear'; // remove the field entirely`. Scans from the declaration
|
|
304
|
+
* line forward, collecting every quoted token on each line, and stops at
|
|
305
|
+
* the first line whose (comment-INCLUDING) text still carries a `;` — the
|
|
306
|
+
* statement terminator ends the union regardless of any trailing comment.
|
|
307
|
+
*/
|
|
308
|
+
function readPolicyUnion(text) {
|
|
309
|
+
const lines = text.split('\n');
|
|
310
|
+
const members = [];
|
|
311
|
+
let inUnion = false;
|
|
312
|
+
for (const line of lines) {
|
|
313
|
+
if (!inUnion) {
|
|
314
|
+
if (!POLICY_UNION_START_RE.test(line)) continue;
|
|
315
|
+
inUnion = true;
|
|
316
|
+
}
|
|
317
|
+
POLICY_UNION_MEMBER_RE.lastIndex = 0;
|
|
318
|
+
let m;
|
|
319
|
+
while ((m = POLICY_UNION_MEMBER_RE.exec(line)) !== null) {
|
|
320
|
+
members.push(m[1]);
|
|
321
|
+
}
|
|
322
|
+
if (line.includes(';')) break;
|
|
323
|
+
}
|
|
324
|
+
return members;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// `getFieldClassification(` called with a quoted string-literal argument —
|
|
328
|
+
// the field-name-keyed dispatch shape. `getFieldClassification(variable)`
|
|
329
|
+
// (a bare identifier, no quote) never matches this pattern, by construction
|
|
330
|
+
// (the quote-character backreference requires an opening quote immediately
|
|
331
|
+
// inside the parens) — the correct, table-driven shape is silent here.
|
|
332
|
+
const FIELD_NAME_DISPATCH_RE = /getFieldClassification\s*\(\s*(['"`])([^'"`]+)\1\s*\)/g;
|
|
333
|
+
|
|
334
|
+
// `field === '<literal>'` / `field !== '<literal>'`, and the reversed
|
|
335
|
+
// `'<literal>' === field` — the field-name-keyed BRANCH shape (as opposed to
|
|
336
|
+
// `FIELD_NAME_DISPATCH_RE`'s field-name-keyed CALL shape above; both report
|
|
337
|
+
// the same `REASON.FIELD_NAME_DISPATCH`, since both are "a branch selected
|
|
338
|
+
// by field name", ADR-3408 §8.1's exact prohibition). This is the shape a
|
|
339
|
+
// bypass takes when it routes AROUND `getFieldClassification` entirely
|
|
340
|
+
// rather than through it — ADR-3408 Decision 5's "route the bypass through
|
|
341
|
+
// a wrapper or a differently-named local" gaming route.
|
|
342
|
+
//
|
|
343
|
+
// Deliberately scoped to ONLY an identifier literally named `field` — the
|
|
344
|
+
// dispatch loop's own loop variable declared at
|
|
345
|
+
// `for (const field of Object.keys(FIELD_CLASSIFICATION))` a few dozen lines
|
|
346
|
+
// below in this same file. This is a DECLARED, narrow limitation, not a
|
|
347
|
+
// silent one: a rename of the loop variable would evade this detector
|
|
348
|
+
// entirely, and an unrelated local elsewhere in this file that happens to
|
|
349
|
+
// also be named `field` would false-positive. Both risks are accepted
|
|
350
|
+
// in trade for avoiding a name-agnostic match, which would flag every
|
|
351
|
+
// unrelated `===`/`!==` string comparison in the file (there are many —
|
|
352
|
+
// e.g. `derivedName !== MILESTONE_PLACEHOLDER`-shaped guards) and bury the
|
|
353
|
+
// real signal in noise; per this guard's own header, over-reporting a
|
|
354
|
+
// comment is an accepted risk but over-reporting live code this broadly is
|
|
355
|
+
// not.
|
|
356
|
+
//
|
|
357
|
+
// The reversed `!==` form (`'<literal>' !== field`) is deliberately NOT
|
|
358
|
+
// matched — not observed anywhere in this codebase, and left out rather
|
|
359
|
+
// than speculatively widened past what was found in practice.
|
|
360
|
+
const FIELD_VAR_EQ_LITERAL_RE = /\bfield\s*(?:===|!==)\s*(['"`])([^'"`]+)\1/g;
|
|
361
|
+
const LITERAL_EQ_FIELD_VAR_RE = /(['"`])([^'"`]+)\1\s*===\s*\bfield\b/g;
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* AXIS 1a: every `getFieldClassification('<literal>')` CALL, and every
|
|
365
|
+
* `field === '<literal>'` / `field !== '<literal>'` / `'<literal>' ===
|
|
366
|
+
* field` BRANCH, inside the executor is a field-name-keyed branch (§8.1).
|
|
367
|
+
* Only ever called against `EXECUTOR_FILE` — `collect()` gates the call
|
|
368
|
+
* site, mirroring `findPolicyDispatchDrift`'s own "only when rel ===
|
|
369
|
+
* EXECUTOR_FILE" rule from the spec this guard was authored against.
|
|
370
|
+
* `preservation === '<member>'` comparisons — the CORRECT policy-dispatch
|
|
371
|
+
* shape `findUnimplementedPolicies` requires to exist — are unaffected: the
|
|
372
|
+
* identifier compared there is `preservation`, never `field`, so
|
|
373
|
+
* `FIELD_VAR_EQ_LITERAL_RE`'s `\bfield\b` anchor does not reach them.
|
|
374
|
+
*/
|
|
375
|
+
function findPolicyDispatchDrift(rel, text) {
|
|
376
|
+
const out = [];
|
|
377
|
+
const stripped = stripComments(text);
|
|
378
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
379
|
+
const line = stripped[i];
|
|
380
|
+
if (!line.trim()) continue;
|
|
381
|
+
// `file` is sanitized here, at construction, not just at the human
|
|
382
|
+
// formatter: `rel` is exactly as attacker-controlled as `source` on a
|
|
383
|
+
// fork PR (a tracked filename can legally carry C1 bytes or bidi
|
|
384
|
+
// overrides), and it reaches `--json` stdout unfiltered otherwise — see
|
|
385
|
+
// `sanitizeForReport`'s own header.
|
|
386
|
+
FIELD_NAME_DISPATCH_RE.lastIndex = 0;
|
|
387
|
+
let m;
|
|
388
|
+
while ((m = FIELD_NAME_DISPATCH_RE.exec(line)) !== null) {
|
|
389
|
+
out.push({
|
|
390
|
+
reason: REASON.FIELD_NAME_DISPATCH,
|
|
391
|
+
axis: 'policy-dispatch',
|
|
392
|
+
file: sanitizeForReport(rel),
|
|
393
|
+
line: i + 1,
|
|
394
|
+
// `field` is captured straight out of a quoted string literal in
|
|
395
|
+
// repo source — attacker-controlled on the same fork-PR basis as
|
|
396
|
+
// `file`/`source`, so sanitize it too rather than let it reach
|
|
397
|
+
// `--json` stdout.
|
|
398
|
+
field: sanitizeForReport(m[2]),
|
|
399
|
+
source: sanitizeForReport(line.trim()),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
FIELD_VAR_EQ_LITERAL_RE.lastIndex = 0;
|
|
403
|
+
while ((m = FIELD_VAR_EQ_LITERAL_RE.exec(line)) !== null) {
|
|
404
|
+
out.push({
|
|
405
|
+
reason: REASON.FIELD_NAME_DISPATCH,
|
|
406
|
+
axis: 'policy-dispatch',
|
|
407
|
+
file: sanitizeForReport(rel),
|
|
408
|
+
line: i + 1,
|
|
409
|
+
field: sanitizeForReport(m[2]),
|
|
410
|
+
source: sanitizeForReport(line.trim()),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
LITERAL_EQ_FIELD_VAR_RE.lastIndex = 0;
|
|
414
|
+
while ((m = LITERAL_EQ_FIELD_VAR_RE.exec(line)) !== null) {
|
|
415
|
+
out.push({
|
|
416
|
+
reason: REASON.FIELD_NAME_DISPATCH,
|
|
417
|
+
axis: 'policy-dispatch',
|
|
418
|
+
file: sanitizeForReport(rel),
|
|
419
|
+
line: i + 1,
|
|
420
|
+
field: sanitizeForReport(m[2]),
|
|
421
|
+
source: sanitizeForReport(line.trim()),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* AXIS 1b: every `FieldPreservation` member (read from `text` via
|
|
430
|
+
* `readPolicyUnion`, so the check cannot itself drift from the union) that
|
|
431
|
+
* has no `preservation === '<member>'` comparison anywhere in the
|
|
432
|
+
* comment-stripped executor source is a declared policy with no executor —
|
|
433
|
+
* §8.1's mirror defect, one level up (a whole MEMBER unimplemented, not just
|
|
434
|
+
* one dispatch call keyed on a field name). `derive` and `clear` are the
|
|
435
|
+
* live instances ADR-3408 §8.6 names.
|
|
436
|
+
*/
|
|
437
|
+
function findUnimplementedPolicies(text, rel) {
|
|
438
|
+
const members = readPolicyUnion(text);
|
|
439
|
+
const strippedText = stripComments(text).join('\n');
|
|
440
|
+
const out = [];
|
|
441
|
+
for (const member of members) {
|
|
442
|
+
const memberRe = new RegExp(`preservation\\s*===\\s*'${escapeRegex(member)}'`);
|
|
443
|
+
if (memberRe.test(strippedText)) continue;
|
|
444
|
+
// `file` and `policy` are sanitized here for the same reason as
|
|
445
|
+
// `findPolicyDispatchDrift` above: both `rel` and a `FieldPreservation`
|
|
446
|
+
// union member are attacker-controlled on a fork PR, exactly like
|
|
447
|
+
// `source`.
|
|
448
|
+
out.push({
|
|
449
|
+
reason: REASON.UNIMPLEMENTED_POLICY,
|
|
450
|
+
axis: 'policy-dispatch',
|
|
451
|
+
file: sanitizeForReport(rel),
|
|
452
|
+
line: 0,
|
|
453
|
+
policy: sanitizeForReport(member),
|
|
454
|
+
source: sanitizeForReport(`FieldPreservation member '${member}' has no executor`),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// AXIS 3 (§8.3(b), closed Phase 2 / #3469): `stateReplaceField(<contentArg>,
|
|
461
|
+
// <fieldArg>, ...)` on a single line, capturing both argument expressions.
|
|
462
|
+
// `contentArg` must be a bare identifier (a call expression or property
|
|
463
|
+
// access as the first argument is not matched — silently out of scope, per
|
|
464
|
+
// this axis's own narrow-limitation note below) so its assignments can be
|
|
465
|
+
// tracked; `fieldArg` is everything up to the next comma, trimmed, so its
|
|
466
|
+
// literal-vs-variable shape can be read off directly.
|
|
467
|
+
const STATE_REPLACE_FIELD_CALL_RE = /\bstateReplaceField\s*\(\s*([A-Za-z_$][\w$]*)\s*,\s*([^,()]+),/g;
|
|
468
|
+
|
|
469
|
+
// True when `arg` (already trimmed) is a fixed string/template literal —
|
|
470
|
+
// the safe shape, since every literal field name this codebase actually
|
|
471
|
+
// uses is a Title-Case body label that cannot collide with a lowercase/
|
|
472
|
+
// snake_case YAML frontmatter key.
|
|
473
|
+
function isQuotedLiteralArg(arg) {
|
|
474
|
+
const t = arg.trim();
|
|
475
|
+
return t.startsWith("'") || t.startsWith('"') || t.startsWith('`');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* The nearest assignment to `varName` (`varName = <expr>` or
|
|
480
|
+
* `const|let|var varName = <expr>`), scanning `lines` BACKWARD from `index`
|
|
481
|
+
* (inclusive) and stopping at the nearest preceding named-function
|
|
482
|
+
* declaration. Returns the assigned expression's trimmed text, or `null`
|
|
483
|
+
* when no such assignment is found before the boundary — meaning `varName`
|
|
484
|
+
* is the enclosing function's own untouched parameter.
|
|
485
|
+
*
|
|
486
|
+
* Deliberately single-hop: this reports whatever the NEAREST assignment's
|
|
487
|
+
* right-hand side literally is, and does not itself follow a further alias
|
|
488
|
+
* (`let body = someOtherVar;` is reported as `"someOtherVar"`, not resolved
|
|
489
|
+
* further). Every real call site in this file assigns its body variable
|
|
490
|
+
* directly from `stripFrontmatter(content)` with no intermediate alias
|
|
491
|
+
* (`updateCore`, `patchCore`, `beginPhaseCore`'s `tryField` helper) — a
|
|
492
|
+
* future call site that introduces one extra hop of aliasing would evade
|
|
493
|
+
* this check. A declared, narrow limitation, not a silent one — mirrors
|
|
494
|
+
* this file's existing precedent (`FIELD_VAR_EQ_LITERAL_RE`'s own
|
|
495
|
+
* documented scope) of accepting a bounded risk in trade for not chasing
|
|
496
|
+
* full dataflow, which is exactly what made the Phase 1 approximation
|
|
497
|
+
* unusable (29 false positives to 1 true positive).
|
|
498
|
+
*/
|
|
499
|
+
function nearestPrecedingAssignment(lines, index, varName) {
|
|
500
|
+
const assignRe = new RegExp(`(?:^|[^.\\w$])(?:const|let|var)?\\s*${escapeRegex(varName)}\\s*=\\s*([^=].*)$`);
|
|
501
|
+
for (let i = index; i >= 0; i--) {
|
|
502
|
+
if (FUNCTION_DECL_LINE_RE.test(lines[i])) return null;
|
|
503
|
+
const m = assignRe.exec(lines[i]);
|
|
504
|
+
if (m) return m[1].trim();
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* AXIS 3: every `stateReplaceField(` call in `EXECUTOR_FILE` whose field-name
|
|
511
|
+
* argument is a VARIABLE (not a quoted literal) — the only shape that can
|
|
512
|
+
* ever rewrite YAML frontmatter, since `stateReplaceField`'s `^field:` line
|
|
513
|
+
* pattern is case-insensitive and matches any line starting with that name,
|
|
514
|
+
* literal or not — AND whose content argument was not assigned from
|
|
515
|
+
* `stripFrontmatter(` at the nearest preceding assignment. A literal
|
|
516
|
+
* field-name argument is never flagged regardless of stripping: every fixed
|
|
517
|
+
* string this file's `stateReplaceField` calls use is a Title-Case body
|
|
518
|
+
* label (`'Phase'`, `'Total Plans in Phase'`, ...) that cannot collide with
|
|
519
|
+
* a lowercase/snake_case frontmatter key by construction, so checking its
|
|
520
|
+
* content argument would only add false positives on the ~20 already-safe
|
|
521
|
+
* `sectionBody`-scoped calls this axis must NOT report (mirrors
|
|
522
|
+
* `updateCore`'s strip-then-replace shape, and `beginPhaseCore`'s
|
|
523
|
+
* `stateReplaceField(body, name, value)`, both legitimately unflagged).
|
|
524
|
+
*/
|
|
525
|
+
function findUnstrippedContentWrites(rel, text) {
|
|
526
|
+
const rawLines = text.split('\n');
|
|
527
|
+
const stripped = stripComments(text);
|
|
528
|
+
const out = [];
|
|
529
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
530
|
+
const line = stripped[i];
|
|
531
|
+
if (!line.trim()) continue;
|
|
532
|
+
STATE_REPLACE_FIELD_CALL_RE.lastIndex = 0;
|
|
533
|
+
let m;
|
|
534
|
+
while ((m = STATE_REPLACE_FIELD_CALL_RE.exec(line)) !== null) {
|
|
535
|
+
const contentArg = m[1];
|
|
536
|
+
const fieldArg = m[2];
|
|
537
|
+
if (isQuotedLiteralArg(fieldArg)) continue;
|
|
538
|
+
const assignment = nearestPrecedingAssignment(stripped, i - 1, contentArg);
|
|
539
|
+
const isStripped = assignment !== null && /^stripFrontmatter\s*\(/.test(assignment);
|
|
540
|
+
if (isStripped) continue;
|
|
541
|
+
// `file`/`source` sanitized for the same fork-PR reason as every other
|
|
542
|
+
// finding in this guard; `contentArg` is captured out of repo source
|
|
543
|
+
// (an identifier name), attacker-controlled on the same basis.
|
|
544
|
+
out.push({
|
|
545
|
+
reason: REASON.UNSTRIPPED_CONTENT_WRITE,
|
|
546
|
+
axis: 'frontmatter-write',
|
|
547
|
+
file: sanitizeForReport(rel),
|
|
548
|
+
line: i + 1,
|
|
549
|
+
field: sanitizeForReport(contentArg),
|
|
550
|
+
source: sanitizeForReport(rawLines[i].trim()),
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return out;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// AXIS 2 (§8.6, retained): `fs.writeFileSync(<targetArg>, ...)`, capturing
|
|
558
|
+
// the target-path argument up to the next comma. The type system (ADR-3473
|
|
559
|
+
// §8.6's `StateTransaction` constructors) closes the `writeStateMd`/
|
|
560
|
+
// `syncStateFrontmatter`/`applyPostSyncPreservation` bypass shape this guard
|
|
561
|
+
// used to scan for by function name; it cannot close a call site that skips
|
|
562
|
+
// those functions ENTIRELY and reaches for Node's raw `fs` module directly —
|
|
563
|
+
// that residual risk is what this axis stays alive to catch.
|
|
564
|
+
const RAW_WRITE_CALL_START_RE = /\bfs\.writeFileSync\s*\(/g;
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Capture `fs.writeFileSync`'s first-argument text starting at `startIdx`
|
|
568
|
+
* (the offset right after its opening `(`), stopping at the first TOP-LEVEL
|
|
569
|
+
* comma or the call's own closing paren — bracket/paren/brace depth and
|
|
570
|
+
* string-literal spans are tracked so a nested call in the target expression
|
|
571
|
+
* (e.g. `path.join(cwd, 'STATE.md')`) does not stop the scan at ITS internal
|
|
572
|
+
* comma. A naive `[^,]+` capture (the guard's prior encoding) stopped at
|
|
573
|
+
* `path.join(cwd` for exactly that shape, silently missing every
|
|
574
|
+
* `fs.writeFileSync(path.join(cwd, 'STATE.md'), …)` call in the wild
|
|
575
|
+
* (found via `tests/state-write-path-drift-guard.test.cjs` F1: "guard:
|
|
576
|
+
* fs.writeFileSync against a STATE.md literal is reported").
|
|
577
|
+
*/
|
|
578
|
+
function captureFirstArg(line, startIdx) {
|
|
579
|
+
let depth = 0;
|
|
580
|
+
let inStr = null;
|
|
581
|
+
let i = startIdx;
|
|
582
|
+
for (; i < line.length; i++) {
|
|
583
|
+
const c = line[i];
|
|
584
|
+
if (inStr) {
|
|
585
|
+
if (c === '\\') { i++; continue; }
|
|
586
|
+
if (c === inStr) inStr = null;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (c === '\'' || c === '"' || c === '`') { inStr = c; continue; }
|
|
590
|
+
if (c === '(' || c === '[' || c === '{') { depth++; continue; }
|
|
591
|
+
if (c === ')' || c === ']' || c === '}') {
|
|
592
|
+
if (depth === 0) break; // the call's own closing paren — no comma found
|
|
593
|
+
depth--;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (c === ',' && depth === 0) break;
|
|
597
|
+
}
|
|
598
|
+
return line.slice(startIdx, i);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// True when the captured target-path expression plausibly names the STATE.md
|
|
602
|
+
// path: either the canonical `statePath` identifier this codebase uses at
|
|
603
|
+
// every real write site (see `src/state.cts`'s `writeStateMd`,
|
|
604
|
+
// `readModifyWriteStateMd`, `cmdStateMilestoneSwitch`), or a literal/template
|
|
605
|
+
// segment containing `STATE.md` outright.
|
|
606
|
+
function targetsStatePath(arg) {
|
|
607
|
+
return /\bstatePath\b/.test(arg) || /STATE\.md/.test(arg);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* AXIS 2: every `fs.writeFileSync(` call in `text` whose target argument
|
|
612
|
+
* names the state path is a raw write that bypasses BOTH the write seam
|
|
613
|
+
* (`writeStateMd` / `syncAndPreserveStateMd`) and the OS Shell Projection
|
|
614
|
+
* seam (`platformWriteSync`, `src/shell-command-projection.cts`) — no
|
|
615
|
+
* legitimate call site in this codebase writes STATE.md this way today
|
|
616
|
+
* (every real writer goes through `platformWriteSync`, whose OWN internal
|
|
617
|
+
* `fs.writeFileSync` calls take a generic `filePath`/`tmpPath` argument, not
|
|
618
|
+
* `statePath`, and are therefore never matched by `targetsStatePath` above).
|
|
619
|
+
* Unratcheted, unexempted: any occurrence is a violation.
|
|
620
|
+
*/
|
|
621
|
+
function findRawStateWrites(rel, text) {
|
|
622
|
+
const rawLines = text.split('\n');
|
|
623
|
+
const stripped = stripComments(text);
|
|
624
|
+
const out = [];
|
|
625
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
626
|
+
const line = stripped[i];
|
|
627
|
+
if (!line.trim()) continue;
|
|
628
|
+
RAW_WRITE_CALL_START_RE.lastIndex = 0;
|
|
629
|
+
let m;
|
|
630
|
+
while ((m = RAW_WRITE_CALL_START_RE.exec(line)) !== null) {
|
|
631
|
+
const argStart = m.index + m[0].length;
|
|
632
|
+
const targetArg = captureFirstArg(line, argStart).trim();
|
|
633
|
+
if (!targetsStatePath(targetArg)) continue;
|
|
634
|
+
// `file`/`source` sanitized for the same fork-PR reason as every other
|
|
635
|
+
// finding in this guard.
|
|
636
|
+
out.push({
|
|
637
|
+
reason: REASON.RAW_STATE_WRITE,
|
|
638
|
+
axis: 'raw-write',
|
|
639
|
+
file: sanitizeForReport(rel),
|
|
640
|
+
line: i + 1,
|
|
641
|
+
source: sanitizeForReport(rawLines[i].trim()),
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return out;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// The two write-seam STAGE functions, matched only as CALLS (`\(`
|
|
649
|
+
// immediately after, modulo whitespace) — never as bare mentions of the
|
|
650
|
+
// name. `writeStateMd(` is deliberately NOT included here (that arm is
|
|
651
|
+
// retired — ADR-3473 §8.6 gates it at the type level instead).
|
|
652
|
+
const SEAM_CALL_RE = /\b(syncStateFrontmatter|applyPostSyncPreservation)\s*\(/g;
|
|
653
|
+
// A line that IS one of the two seam stage functions' own definitions —
|
|
654
|
+
// skipped outright, never counted as a call to itself.
|
|
655
|
+
const SEAM_DEF_LINE_RE = /^\s*(?:export\s+)?(?:async\s+)?function\s+(?:syncStateFrontmatter|applyPostSyncPreservation)\b/;
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* AXIS 5 (§8.3, RETAINED, issue #3871): every direct `syncStateFrontmatter(`/
|
|
659
|
+
* `applyPostSyncPreservation(` call in `text`, outside the two functions' own
|
|
660
|
+
* definitions and (only inside `SEAM_OWNER_FILE`) outside
|
|
661
|
+
* `SEAM_OWNER_EXEMPT_FUNCTIONS`'s own bodies. Terminal: unlike the old
|
|
662
|
+
* `findSeamBypasses` this axis descends from, there is no ratchet — any
|
|
663
|
+
* occurrence is `REASON.COMPOSITION_BYPASS` directly.
|
|
664
|
+
*/
|
|
665
|
+
function findCompositionBypasses(rel, text) {
|
|
666
|
+
const rawLines = text.split('\n');
|
|
667
|
+
const stripped = stripComments(text);
|
|
668
|
+
const out = [];
|
|
669
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
670
|
+
const line = stripped[i];
|
|
671
|
+
if (!line.trim()) continue;
|
|
672
|
+
if (SEAM_DEF_LINE_RE.test(line)) continue;
|
|
673
|
+
SEAM_CALL_RE.lastIndex = 0;
|
|
674
|
+
let m;
|
|
675
|
+
while ((m = SEAM_CALL_RE.exec(line)) !== null) {
|
|
676
|
+
if (rel === SEAM_OWNER_FILE) {
|
|
677
|
+
const fn = enclosingFunction(stripped, i);
|
|
678
|
+
if (fn && SEAM_OWNER_EXEMPT_FUNCTIONS.includes(fn)) continue;
|
|
679
|
+
}
|
|
680
|
+
// `file`/`source` sanitized for the same fork-PR reason as every other
|
|
681
|
+
// finding in this guard.
|
|
682
|
+
out.push({
|
|
683
|
+
reason: REASON.COMPOSITION_BYPASS,
|
|
684
|
+
axis: 'composition-bypass',
|
|
685
|
+
file: sanitizeForReport(rel),
|
|
686
|
+
line: i + 1,
|
|
687
|
+
symbol: m[1],
|
|
688
|
+
source: sanitizeForReport(rawLines[i].trim()),
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return out;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Prose in the prompt layer shelling out to a write-side `gsd-tools`
|
|
696
|
+
// subcommand — the SAME write seam, expressed as markdown instructing an
|
|
697
|
+
// agent to run a command, rather than TypeScript calling a function
|
|
698
|
+
// directly (Decision 4(d)'s "the scan surface is declared, and is not just
|
|
699
|
+
// `src/`"). Matched on RAW lines — no comment stripping — because markdown
|
|
700
|
+
// carries no comment syntax this guard should be stripping in the first
|
|
701
|
+
// place. `g`-flagged so multiple candidate occurrences on one line are all
|
|
702
|
+
// checked against backtick spans below, rather than only the first.
|
|
703
|
+
const PROMPT_SEAM_RE = /gsd[-_]?tools[^\n]*\b(state\.patch|state\.planned-phase|state\.sync|phase\.complete)\b/g;
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Every `` `...` `` inline-code span on `line`, as `[start, end)` character
|
|
707
|
+
* ranges (end exclusive). Handles multiple spans on one line correctly by
|
|
708
|
+
* repeated `exec` over a global, non-overlapping backtick-pair pattern —
|
|
709
|
+
* unlike a naive "count backticks before the match" parity check, this does
|
|
710
|
+
* not get confused by a line that mixes code spans with unrelated literal
|
|
711
|
+
* backticks (e.g. an unmatched one in prose).
|
|
712
|
+
*/
|
|
713
|
+
const CODE_SPAN_RE = /`[^`\n]+`/g;
|
|
714
|
+
function codeSpanRanges(line) {
|
|
715
|
+
const ranges = [];
|
|
716
|
+
CODE_SPAN_RE.lastIndex = 0;
|
|
717
|
+
let m;
|
|
718
|
+
while ((m = CODE_SPAN_RE.exec(line)) !== null) {
|
|
719
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
720
|
+
}
|
|
721
|
+
return ranges;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* True when character offset `index` of `line` falls inside one of `line`'s
|
|
726
|
+
* inline-code spans.
|
|
727
|
+
*/
|
|
728
|
+
function isInsideCodeSpan(line, index) {
|
|
729
|
+
return codeSpanRanges(line).some(([start, end]) => index >= start && index < end);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* AXIS 4: every prompt-layer line instructing an agent to shell out to a
|
|
734
|
+
* write-side `gsd-tools` subcommand. Terminal (unratcheted): any occurrence
|
|
735
|
+
* is a violation — this baseline was always empty for the prompt layer (no
|
|
736
|
+
* prompt-layer entry was ever acknowledged), so removing the ratchet changes
|
|
737
|
+
* nothing observable here.
|
|
738
|
+
*
|
|
739
|
+
* A candidate occurrence enclosed in backticks is a MENTION, not an
|
|
740
|
+
* invocation, and is deliberately not reported — CONTRIBUTING.md's "Every
|
|
741
|
+
* `commit` invocation in shipped content must declare `--files`" section
|
|
742
|
+
* states the repo's settled convention verbatim: "Write the command
|
|
743
|
+
* reference in backticks — the repo's own convention — and it is correctly
|
|
744
|
+
* read as a mention." ADR-3180 Amendment 3 records the cost of getting this
|
|
745
|
+
* wrong: the first `lint-phase-enumeration-drift.cjs` flagged JSDoc that
|
|
746
|
+
* merely documented the canonical owner as drift, which trains readers to
|
|
747
|
+
* reflexively exempt documentation instead of trusting the guard — the
|
|
748
|
+
* opposite of Decision 4(a)'s intent.
|
|
749
|
+
*/
|
|
750
|
+
function findPromptSeamUses(rel, text) {
|
|
751
|
+
const lines = text.split('\n');
|
|
752
|
+
const out = [];
|
|
753
|
+
for (let i = 0; i < lines.length; i++) {
|
|
754
|
+
const line = lines[i];
|
|
755
|
+
PROMPT_SEAM_RE.lastIndex = 0;
|
|
756
|
+
let m;
|
|
757
|
+
while ((m = PROMPT_SEAM_RE.exec(line)) !== null) {
|
|
758
|
+
if (isInsideCodeSpan(line, m.index)) continue;
|
|
759
|
+
// `file` is sanitized here for the same reason as every other finding
|
|
760
|
+
// in this guard: `rel` is attacker-controlled on a fork PR, exactly
|
|
761
|
+
// like `source`.
|
|
762
|
+
out.push({
|
|
763
|
+
reason: REASON.PROMPT_LAYER_STATE_WRITE,
|
|
764
|
+
axis: 'write-seam',
|
|
765
|
+
file: sanitizeForReport(rel),
|
|
766
|
+
line: i + 1,
|
|
767
|
+
symbol: 'prompt-layer-state-write',
|
|
768
|
+
source: sanitizeForReport(line.trim()),
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return out;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Run both scan passes (the `src/` tree for Axis 1 + Axis 2 + Axis 3 + Axis 5,
|
|
777
|
+
* the prompt layer for Axis 4) and return the flat, already-terminal finding
|
|
778
|
+
* list — every finding this guard produces carries its own `reason`; there
|
|
779
|
+
* is no longer a ratcheted axis needing a second pass against a baseline.
|
|
780
|
+
*
|
|
781
|
+
* `root` defaults to `REPO_ROOT` (this repo) so every existing caller —
|
|
782
|
+
* `npm run lint:ci`, a bare `node scripts/lint-state-write-path-drift.cjs`,
|
|
783
|
+
* every other module that `require`s `collect` with no argument — is
|
|
784
|
+
* byte-identical to before this parameter existed. It is overridable so a
|
|
785
|
+
* test can exercise the guard's real scanning/reporting logic against a
|
|
786
|
+
* throwaway synthetic tree instead of mutating this repository's own `src/`
|
|
787
|
+
* to prove the guard can fail (see `--root` on the CLI, and F2 in
|
|
788
|
+
* `tests/state-write-path-drift-guard.test.cjs`).
|
|
789
|
+
*/
|
|
790
|
+
function collect(root = REPO_ROOT) {
|
|
791
|
+
const srcFindings = scanTree({
|
|
792
|
+
root,
|
|
793
|
+
scanDirs: SRC_DIRS,
|
|
794
|
+
scanExt: SRC_EXT,
|
|
795
|
+
onFile(rel, text) {
|
|
796
|
+
const relPosix = toPosixRel(rel);
|
|
797
|
+
const found = [];
|
|
798
|
+
if (relPosix === EXECUTOR_FILE) {
|
|
799
|
+
found.push(...findPolicyDispatchDrift(relPosix, text));
|
|
800
|
+
found.push(...findUnimplementedPolicies(text, relPosix));
|
|
801
|
+
found.push(...findUnstrippedContentWrites(relPosix, text));
|
|
802
|
+
}
|
|
803
|
+
found.push(...findRawStateWrites(relPosix, text));
|
|
804
|
+
found.push(...findCompositionBypasses(relPosix, text));
|
|
805
|
+
return found;
|
|
806
|
+
},
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
const promptFindings = scanTree({
|
|
810
|
+
root,
|
|
811
|
+
scanDirs: PROMPT_DIRS,
|
|
812
|
+
scanExt: PROMPT_EXT,
|
|
813
|
+
onFile(rel, text) {
|
|
814
|
+
return findPromptSeamUses(toPosixRel(rel), text);
|
|
815
|
+
},
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
return { findings: [...srcFindings, ...promptFindings] };
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const GOODHART_NOTE =
|
|
822
|
+
'Goodhart note (ADR-3408 Decision 5): this "0 write-path violations" is a LAGGING metric — report ' +
|
|
823
|
+
'it only alongside the behavioral identity test\'s result (asserted at the consumer\'s output), ' +
|
|
824
|
+
'never alone.';
|
|
825
|
+
|
|
826
|
+
function printFindings(findings) {
|
|
827
|
+
for (const f of findings) {
|
|
828
|
+
process.stderr.write(`[${f.reason}] ${sanitizeForReport(f.file)}:${f.line}\n`);
|
|
829
|
+
process.stderr.write(` ${sanitizeForReport(f.source)}\n`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Parse `argv` into `{ root, wantJson, unknown }`. `--root <dir>` overrides
|
|
835
|
+
* the scan root (default `REPO_ROOT`, resolved relative to `process.cwd()`
|
|
836
|
+
* when given); `--json` is a bare flag. Anything else lands in `unknown` so
|
|
837
|
+
* `main` can report a usage error rather than silently ignoring a typo.
|
|
838
|
+
*/
|
|
839
|
+
function parseArgs(argv) {
|
|
840
|
+
const args = argv || [];
|
|
841
|
+
let root = REPO_ROOT;
|
|
842
|
+
let wantJson = false;
|
|
843
|
+
const unknown = [];
|
|
844
|
+
for (let i = 0; i < args.length; i++) {
|
|
845
|
+
const a = args[i];
|
|
846
|
+
if (a === '--json') {
|
|
847
|
+
wantJson = true;
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
if (a === '--root') {
|
|
851
|
+
const value = args[i + 1];
|
|
852
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
853
|
+
return { error: '--root requires a directory argument' };
|
|
854
|
+
}
|
|
855
|
+
root = path.resolve(value);
|
|
856
|
+
i += 1;
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
unknown.push(a);
|
|
860
|
+
}
|
|
861
|
+
return { root, wantJson, unknown };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* `argv`: `--json` prints the machine-readable finding set instead of the
|
|
866
|
+
* human-readable report; `--root <dir>` overrides the scan root (defaults to
|
|
867
|
+
* this repo — see `parseArgs`'s own docstring). Exit codes: 0 clean, 1 a
|
|
868
|
+
* finding was reported, 2 usage error.
|
|
869
|
+
*/
|
|
870
|
+
function main(argv) {
|
|
871
|
+
const parsed = parseArgs(argv);
|
|
872
|
+
if (parsed.error) {
|
|
873
|
+
process.stderr.write(`lint-state-write-path-drift: ${parsed.error}\n`);
|
|
874
|
+
process.exitCode = 2;
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const { root, wantJson, unknown } = parsed;
|
|
878
|
+
if (unknown.length > 0) {
|
|
879
|
+
process.stderr.write(
|
|
880
|
+
`lint-state-write-path-drift: unrecognized argument(s): ${unknown.map((a) => sanitizeForReport(a)).join(', ')} ` +
|
|
881
|
+
'(expected --json and/or --root <dir>)\n',
|
|
882
|
+
);
|
|
883
|
+
process.exitCode = 2;
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const { findings } = collect(root);
|
|
888
|
+
const summary = {
|
|
889
|
+
policyDispatchViolations: findings.filter((f) => f.axis === 'policy-dispatch').length,
|
|
890
|
+
frontmatterWriteViolations: findings.filter((f) => f.axis === 'frontmatter-write').length,
|
|
891
|
+
rawWriteViolations: findings.filter((f) => f.axis === 'raw-write').length,
|
|
892
|
+
writeSeamViolations: findings.filter((f) => f.axis === 'write-seam').length,
|
|
893
|
+
compositionBypassViolations: findings.filter((f) => f.axis === 'composition-bypass').length,
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
if (wantJson) {
|
|
897
|
+
process.stdout.write(`${JSON.stringify({ ok: findings.length === 0, findings, summary }, null, 2)}\n`);
|
|
898
|
+
process.exitCode = findings.length === 0 ? 0 : 1;
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (findings.length === 0) {
|
|
903
|
+
process.stdout.write(
|
|
904
|
+
'ok state-write-path-drift: no policy-dispatch, frontmatter-write, raw-write, prompt-layer, or ' +
|
|
905
|
+
'composition-bypass violations found\n',
|
|
906
|
+
);
|
|
907
|
+
process.stdout.write(`${GOODHART_NOTE}\n`);
|
|
908
|
+
process.exitCode = 0;
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
process.stderr.write(
|
|
913
|
+
'state-write-path-drift: violation(s) found (ADR-3408 §8.1/§8.3, ADR-3473 §8.6). See ' +
|
|
914
|
+
'docs/adr/3408-state-write-path-preservation.md and docs/adr/3473-enforcement-by-construction.md ' +
|
|
915
|
+
'for the contract:\n',
|
|
916
|
+
);
|
|
917
|
+
printFindings(findings);
|
|
918
|
+
process.exitCode = 1;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (require.main === module) main(process.argv.slice(2));
|
|
922
|
+
|
|
923
|
+
module.exports = {
|
|
924
|
+
REASON,
|
|
925
|
+
REPO_ROOT,
|
|
926
|
+
SRC_DIRS,
|
|
927
|
+
SRC_EXT,
|
|
928
|
+
PROMPT_DIRS,
|
|
929
|
+
PROMPT_EXT,
|
|
930
|
+
EXECUTOR_FILE,
|
|
931
|
+
SEAM_OWNER_FILE,
|
|
932
|
+
SEAM_OWNER_EXEMPT_FUNCTIONS,
|
|
933
|
+
toPosixRel,
|
|
934
|
+
stripComments,
|
|
935
|
+
enclosingFunction,
|
|
936
|
+
readPolicyUnion,
|
|
937
|
+
findPolicyDispatchDrift,
|
|
938
|
+
findUnimplementedPolicies,
|
|
939
|
+
findUnstrippedContentWrites,
|
|
940
|
+
isQuotedLiteralArg,
|
|
941
|
+
nearestPrecedingAssignment,
|
|
942
|
+
findRawStateWrites,
|
|
943
|
+
targetsStatePath,
|
|
944
|
+
findCompositionBypasses,
|
|
945
|
+
findPromptSeamUses,
|
|
946
|
+
isInsideCodeSpan,
|
|
947
|
+
parseArgs,
|
|
948
|
+
collect,
|
|
949
|
+
main,
|
|
950
|
+
};
|