@opengsd/gsd-core 1.11.0 → 1.13.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/.opencode/plugins/gsd-core.js +12 -0
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-dom-verifier.md +169 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +78 -42
- package/agents/gsd-framework-selector.md +1 -3
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +0 -1
- package/agents/gsd-pattern-mapper.md +11 -0
- package/agents/gsd-phase-researcher.md +3 -1
- package/agents/gsd-plan-checker.md +91 -112
- package/agents/gsd-planner.md +20 -4
- 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 +82 -7
- package/agents/gsd-ui-researcher.md +70 -3
- package/agents/gsd-verifier.md +24 -2
- package/bin/install.js +847 -200
- 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/ns-workflow.md +2 -1
- package/commands/gsd/phase.md +1 -1
- package/commands/gsd/quick-batch.md +105 -0
- package/commands/gsd/quick.md +8 -4
- package/commands/gsd/surface.md +18 -8
- package/gsd-core/bin/gsd-tools.cjs +761 -100
- package/gsd-core/bin/lib/active-workstream-store.cjs +8 -0
- package/gsd-core/bin/lib/adr-parser.cjs +13 -7
- package/gsd-core/bin/lib/agent-install-check.cjs +162 -0
- package/gsd-core/bin/lib/api-coverage.cjs +30 -9
- package/gsd-core/bin/lib/artifacts.cjs +2 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +30 -11
- package/gsd-core/bin/lib/audit.cjs +163 -41
- package/gsd-core/bin/lib/broken-windows.cjs +306 -28
- package/gsd-core/bin/lib/capability-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-lock.cjs +10 -4
- package/gsd-core/bin/lib/capability-registry.cjs +785 -144
- package/gsd-core/bin/lib/capability-state.cjs +25 -4
- package/gsd-core/bin/lib/capability-validator.cjs +321 -18
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +229 -6
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/cli-exit.cjs +496 -10
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
- package/gsd-core/bin/lib/codex-agent-toml.cjs +410 -4
- package/gsd-core/bin/lib/command-aliases.cjs +16 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
- package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
- package/gsd-core/bin/lib/commands.cjs +877 -54
- package/gsd-core/bin/lib/complexity-trigger.cjs +26 -6
- package/gsd-core/bin/lib/config-loader.cjs +121 -29
- package/gsd-core/bin/lib/config.cjs +92 -2
- package/gsd-core/bin/lib/configuration.cjs +129 -37
- package/gsd-core/bin/lib/core-utils.cjs +118 -14
- package/gsd-core/bin/lib/decisions.cjs +213 -1
- package/gsd-core/bin/lib/edge-probe.cjs +23 -2
- 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/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +975 -326
- package/gsd-core/bin/lib/gap-checker.cjs +41 -8
- package/gsd-core/bin/lib/git-base-branch.cjs +182 -39
- package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +7 -3
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +60 -14
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +22 -8
- package/gsd-core/bin/lib/health-diagnostic.cjs +23 -3
- package/gsd-core/bin/lib/host-integration.cjs +96 -11
- package/gsd-core/bin/lib/init-command-router.cjs +132 -21
- package/gsd-core/bin/lib/init.cjs +252 -56
- package/gsd-core/bin/lib/install-engine.cjs +252 -15
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +78 -1
- package/gsd-core/bin/lib/install-profiles.cjs +100 -18
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +10 -7
- package/gsd-core/bin/lib/intel.cjs +101 -26
- package/gsd-core/bin/lib/io.cjs +195 -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/loop-resolver.cjs +14 -8
- package/gsd-core/bin/lib/markdown-table.cjs +175 -4
- package/gsd-core/bin/lib/milestone.cjs +112 -7
- package/gsd-core/bin/lib/model-catalog.cjs +177 -19
- package/gsd-core/bin/lib/model-resolver.cjs +10 -28
- package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
- package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
- package/gsd-core/bin/lib/phase-estimation.cjs +17 -8
- package/gsd-core/bin/lib/phase-id.cjs +321 -13
- package/gsd-core/bin/lib/phase-lifecycle.cjs +24 -16
- package/gsd-core/bin/lib/phase-locator.cjs +138 -17
- package/gsd-core/bin/lib/phase.cjs +1175 -115
- package/gsd-core/bin/lib/plan-document.cjs +273 -0
- package/gsd-core/bin/lib/plan-scan.cjs +13 -2
- 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-snapshot.cjs +165 -34
- package/gsd-core/bin/lib/planning-workspace.cjs +159 -28
- package/gsd-core/bin/lib/probe-core.cjs +4 -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/quick-batch-command-router.cjs +285 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
- package/gsd-core/bin/lib/quick-batch.cjs +840 -0
- package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +71 -45
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +62 -14
- package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
- package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
- package/gsd-core/bin/lib/roadmap-command-router.cjs +45 -31
- package/gsd-core/bin/lib/roadmap-parser.cjs +577 -41
- package/gsd-core/bin/lib/roadmap.cjs +248 -64
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +329 -41
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +320 -109
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +487 -83
- package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
- package/gsd-core/bin/lib/runtime-slash.cjs +72 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +75 -8
- package/gsd-core/bin/lib/smart-entry.cjs +19 -31
- package/gsd-core/bin/lib/spec-section.cjs +12 -7
- package/gsd-core/bin/lib/state-command-router.cjs +47 -18
- package/gsd-core/bin/lib/state-contract.cjs +359 -0
- package/gsd-core/bin/lib/state-document.cjs +216 -5
- package/gsd-core/bin/lib/state-md-schema.cjs +231 -0
- package/gsd-core/bin/lib/state-transition.cjs +850 -145
- package/gsd-core/bin/lib/state.cjs +1629 -287
- package/gsd-core/bin/lib/surface.cjs +33 -10
- 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/tdd-red-evidence.cjs +133 -0
- package/gsd-core/bin/lib/teams-status.cjs +4 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +58 -20
- package/gsd-core/bin/lib/uat.cjs +2542 -387
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
- package/gsd-core/bin/lib/ui-safety-gate.cjs +37 -7
- package/gsd-core/bin/lib/unusable-input.cjs +13 -0
- package/gsd-core/bin/lib/update-context.cjs +6 -2
- package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +230 -12
- package/gsd-core/bin/lib/vendor/README.md +43 -5
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +287 -13
- package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +441 -56
- package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
- package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
- package/gsd-core/bin/lib/worktree-safety.cjs +185 -21
- package/gsd-core/bin/shared/config-defaults.manifest.json +7 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +13 -0
- 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/bin/verify-reapply-patches.cjs +70 -3
- package/gsd-core/references/agent-contracts.md +6 -5
- 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 +37 -19
- package/gsd-core/references/decimal-phase-calculation.md +5 -5
- package/gsd-core/references/edge-probe.md +17 -5
- package/gsd-core/references/execute-mvp-tdd.md +18 -18
- package/gsd-core/references/execute-phase-between-wave-reset.md +9 -12
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/execute-phase-wave-guard.md +11 -9
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/failing-direction.md +78 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- 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 +3 -3
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +22 -0
- package/gsd-core/references/model-profiles.md +1 -1
- package/gsd-core/references/mvp-concepts.md +2 -2
- 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/plan-checker-examples.md +41 -0
- package/gsd-core/references/planner-antipatterns.md +25 -0
- package/gsd-core/references/planner-chunked.md +5 -1
- package/gsd-core/references/planner-coupling.md +42 -0
- package/gsd-core/references/planner-failing-direction.md +53 -0
- package/gsd-core/references/planner-human-verify-mode.md +15 -1
- package/gsd-core/references/planner-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +76 -3
- package/gsd-core/references/planner-verify-command-grounding.md +17 -0
- package/gsd-core/references/planning-config.md +39 -9
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/reviewer-instances.md +31 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/runtime-aware-dispatch.md +1 -1
- package/gsd-core/references/tdd.md +15 -12
- 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 +2 -2
- package/gsd-core/references/verifier-evidence-gate.md +160 -0
- package/gsd-core/references/verify-command-path-resolvability.md +42 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +11 -11
- 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/phase-prompt.md +7 -0
- package/gsd-core/templates/state.md +7 -0
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +3 -1
- package/gsd-core/workflows/add-phase.md +5 -3
- package/gsd-core/workflows/add-tests.md +4 -9
- package/gsd-core/workflows/add-todo.md +2 -2
- package/gsd-core/workflows/ai-integration-phase.md +5 -10
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +14 -3
- package/gsd-core/workflows/audit-milestone.md +11 -9
- package/gsd-core/workflows/audit-uat.md +19 -2
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
- package/gsd-core/workflows/autonomous.md +12 -26
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +3 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +16 -14
- package/gsd-core/workflows/code-review-fix.md +3 -1
- package/gsd-core/workflows/code-review.md +192 -69
- package/gsd-core/workflows/complete-milestone.md +28 -14
- package/gsd-core/workflows/debug.md +6 -4
- package/gsd-core/workflows/diagnose-issues.md +17 -7
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +3 -1
- package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -7
- package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/text.md +3 -1
- package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
- package/gsd-core/workflows/discuss-phase-assumptions.md +3 -3
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +46 -19
- package/gsd-core/workflows/docs-update.md +6 -5
- package/gsd-core/workflows/edit-phase.md +3 -1
- package/gsd-core/workflows/eval-review.md +5 -10
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +129 -11
- 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 +1 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +29 -5
- 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 +4 -2
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +68 -66
- package/gsd-core/workflows/execute-plan.md +25 -20
- package/gsd-core/workflows/explore.md +3 -1
- package/gsd-core/workflows/extract-learnings.md +3 -1
- package/gsd-core/workflows/fast.md +8 -2
- package/gsd-core/workflows/forensics.md +3 -1
- package/gsd-core/workflows/graduation.md +6 -6
- package/gsd-core/workflows/health.md +4 -7
- package/gsd-core/workflows/help/modes/brief.md +2 -0
- package/gsd-core/workflows/help/modes/default.md +2 -0
- package/gsd-core/workflows/help/modes/full.md +12 -0
- package/gsd-core/workflows/help/modes/topic.md +2 -0
- package/gsd-core/workflows/help.md +2 -0
- package/gsd-core/workflows/import.md +17 -14
- package/gsd-core/workflows/inbox.md +5 -6
- package/gsd-core/workflows/ingest-docs.md +45 -12
- package/gsd-core/workflows/insert-phase.md +7 -5
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +7 -3
- package/gsd-core/workflows/list-workspaces.md +3 -1
- package/gsd-core/workflows/manager.md +15 -26
- package/gsd-core/workflows/map-codebase.md +3 -1
- package/gsd-core/workflows/milestone-summary.md +3 -1
- package/gsd-core/workflows/mvp-phase.md +3 -3
- package/gsd-core/workflows/new-milestone.md +10 -22
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
- package/gsd-core/workflows/new-project.md +17 -29
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +4 -2
- package/gsd-core/workflows/node-repair.md +2 -0
- package/gsd-core/workflows/note.md +2 -0
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +20 -5
- 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 +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +4 -4
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +12 -3
- package/gsd-core/workflows/plan-phase.md +251 -54
- package/gsd-core/workflows/plan-review-convergence.md +148 -19
- package/gsd-core/workflows/plant-seed.md +3 -3
- package/gsd-core/workflows/pr-branch.md +195 -51
- package/gsd-core/workflows/profile-user.md +17 -15
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +52 -15
- package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +38 -5
- package/gsd-core/workflows/quick/steps/quick-verification.md +2 -4
- package/gsd-core/workflows/quick/steps/research-phase.md +5 -7
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
- package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
- package/gsd-core/workflows/quick-batch.md +203 -0
- package/gsd-core/workflows/quick.md +33 -32
- package/gsd-core/workflows/reapply-patches.md +2 -0
- package/gsd-core/workflows/remove-phase.md +6 -4
- package/gsd-core/workflows/remove-workspace.md +3 -3
- package/gsd-core/workflows/resume-project.md +14 -14
- package/gsd-core/workflows/review.md +404 -21
- package/gsd-core/workflows/scan.md +3 -1
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +3 -3
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +9 -9
- package/gsd-core/workflows/settings-integrations.md +66 -32
- package/gsd-core/workflows/settings.md +4 -6
- package/gsd-core/workflows/ship.md +22 -16
- package/gsd-core/workflows/sketch-wrap-up.md +13 -17
- package/gsd-core/workflows/sketch.md +13 -19
- package/gsd-core/workflows/smart-entry.md +4 -6
- package/gsd-core/workflows/spec-phase.md +31 -4
- package/gsd-core/workflows/spike-wrap-up.md +9 -11
- package/gsd-core/workflows/spike.md +21 -32
- package/gsd-core/workflows/stats.md +4 -2
- package/gsd-core/workflows/sync-skills.md +13 -5
- package/gsd-core/workflows/thread.md +13 -7
- package/gsd-core/workflows/transition.md +7 -5
- package/gsd-core/workflows/ui-phase.md +36 -21
- package/gsd-core/workflows/ui-review.md +7 -11
- package/gsd-core/workflows/ultraplan-phase.md +7 -13
- package/gsd-core/workflows/undo.md +9 -17
- package/gsd-core/workflows/update.md +47 -48
- 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 +106 -21
- package/hooks/dist/gsd-agent-isolation-guard.js +77 -38
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-config-reload.js +18 -12
- package/hooks/dist/gsd-context-monitor.js +302 -22
- package/hooks/dist/gsd-cursor-post-tool.js +3 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +3 -1
- 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 +28 -23
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -1
- 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 +77 -0
- package/hooks/dist/gsd-phase-boundary.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +46 -12
- package/hooks/dist/gsd-read-guard.js +18 -7
- package/hooks/dist/gsd-read-injection-scanner.js +22 -13
- package/hooks/dist/gsd-secret-read-guard.js +1079 -0
- package/hooks/dist/gsd-session-state.sh +1 -0
- package/hooks/dist/gsd-statusline.js +222 -29
- package/hooks/dist/gsd-validate-commit.sh +523 -12
- 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 +36 -17
- 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 +210 -1
- 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 +36 -6
- package/hooks/dist/managed-hooks-registry.cjs +4 -0
- package/hooks/gsd-agent-isolation-guard.js +77 -38
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-config-reload.js +18 -12
- package/hooks/gsd-context-monitor.js +302 -22
- package/hooks/gsd-cursor-post-tool.js +3 -1
- package/hooks/gsd-cursor-pre-tool.js +3 -1
- package/hooks/gsd-cursor-session-start.js +2 -1
- package/hooks/gsd-cursor-stop.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +28 -23
- package/hooks/gsd-cursor-subagent-stop.js +3 -1
- package/hooks/gsd-ensure-canonical-path.js +2 -1
- package/hooks/gsd-graphify-update.sh +22 -18
- package/hooks/gsd-node-runner.sh +77 -0
- package/hooks/gsd-phase-boundary.sh +1 -0
- package/hooks/gsd-prompt-guard.js +46 -12
- package/hooks/gsd-read-guard.js +18 -7
- package/hooks/gsd-read-injection-scanner.js +22 -13
- package/hooks/gsd-secret-read-guard.js +1079 -0
- package/hooks/gsd-session-state.sh +1 -0
- package/hooks/gsd-statusline.js +222 -29
- package/hooks/gsd-validate-commit.sh +523 -12
- package/hooks/gsd-windsurf-pre-command.js +16 -11
- package/hooks/gsd-windsurf-pre-write.js +22 -13
- package/hooks/gsd-workflow-guard.js +36 -17
- package/hooks/gsd-worktree-path-guard.js +36 -21
- package/hooks/gsd-write-guard.js +35 -25
- package/hooks/hooks.json +6 -0
- package/hooks/lib/cli-exit.js +560 -0
- package/hooks/lib/exit-code-registry.js +98 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/git-probe.js +84 -0
- package/hooks/lib/hook-exit.js +81 -0
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/managed-hooks-registry.cjs +4 -0
- package/package.json +14 -9
- package/scripts/base64-scan.sh +74 -12
- package/scripts/build-hooks.js +12 -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 +52 -12
- package/scripts/ci-timeout-report.cjs +230 -0
- package/scripts/docs-guard-registry.cjs +406 -0
- package/scripts/gen-capability-registry.cjs +8 -6
- 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-hooks-cli-exit.cjs +239 -0
- package/scripts/gen-install-tree-fixtures.cjs +2 -2
- package/scripts/gen-loop-host-contract.cjs +189 -4
- 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/ci-job-timing.cjs +72 -0
- package/scripts/lib/cli-exit.cjs +546 -44
- package/scripts/lib/drift-scan.cjs +32 -2
- package/scripts/lib/exit-code-registry.cjs +98 -0
- package/scripts/lib/ndjson-reporter.cjs +119 -0
- package/scripts/lib/shellcheck-fetch.cjs +247 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-docs-guard-registration.cjs +495 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +198 -0
- package/scripts/lint-eslint-glob-coverage.allowlist.json +4 -0
- package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
- package/scripts/lint-health-diagnostic-rule-table.cjs +65 -8
- package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
- package/scripts/lint-phase-enumeration-drift.cjs +45 -14
- package/scripts/lint-phase-id-drift.cjs +133 -8
- package/scripts/lint-planning-prompt-drift.cjs +38 -1
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-removed-but-needed.cjs +184 -16
- package/scripts/lint-response-language-coverage.cjs +524 -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-write-path-drift.cjs +337 -432
- package/scripts/lint-test-file-count.allowlist.json +124 -4
- package/scripts/lint-test-file-count.cjs +25 -3
- package/scripts/lint-unreachable-guard-drift.cjs +51 -64
- package/scripts/lint-vendored-deps.cjs +208 -35
- package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/mutation-matrix.cjs +599 -50
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +83 -14
- package/scripts/require-issue-link-policy.cjs +16 -1
- package/scripts/secret-scan.sh +75 -13
- package/scripts/select-docs-guards.cjs +56 -0
- package/scripts/sync-runtime-launcher.cjs +22 -3
- package/skills/gsd-discuss-phase/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-import/SKILL.md +1 -1
- package/skills/gsd-ns-workflow/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -1
- package/skills/gsd-quick/SKILL.md +8 -4
- package/skills/gsd-quick-batch/SKILL.md +105 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- package/vscode/package.json +1 -1
- package/bin/lib/ui-safety-gate.cjs +0 -109
- package/scripts/lint-emitted-drift-ack.cjs +0 -344
- package/scripts/state-write-path-drift-baseline.json +0 -19
|
@@ -5,12 +5,45 @@
|
|
|
5
5
|
* Anti-divergence drift guard for the STATE.md WRITE PATH — epic #3408, issue
|
|
6
6
|
* #3468, ADR-3408 Decision 5, contract §8.1/§8.2/§8.3
|
|
7
7
|
* (`docs/adr/3408-state-write-path-preservation.md` is the contract this
|
|
8
|
-
* guard enforces; read it first).
|
|
8
|
+
* guard enforces; read it first) — SHRUNK per ADR-3473 §8.6 (issue #3871).
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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:
|
|
14
47
|
*
|
|
15
48
|
* AXIS 1 — POLICY DISPATCH (§8.1). `applyStatePreservation` must select
|
|
16
49
|
* its branch from a `FIELD_CLASSIFICATION` row's `preservation` value,
|
|
@@ -27,12 +60,39 @@
|
|
|
27
60
|
* undetected by the call-shape check alone). Scoped to the identifier
|
|
28
61
|
* `field` only; see `FIELD_VAR_EQ_LITERAL_RE`'s own comment for why.
|
|
29
62
|
*
|
|
30
|
-
* AXIS 2 — WRITE
|
|
31
|
-
*
|
|
32
|
-
* `
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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.
|
|
36
96
|
*
|
|
37
97
|
* DESIGN CONSTRAINTS (ADR-3180 Decision 4, adopted verbatim by ADR-3408):
|
|
38
98
|
* - 4(a) whole-repo scan, never an allowlist. ADR-3180's own phases found
|
|
@@ -40,15 +100,9 @@
|
|
|
40
100
|
* nothing.
|
|
41
101
|
* - 4(d) the scan surface is DECLARED and is NOT just `src/` — `src/`
|
|
42
102
|
* alone is itself an allowlist one directory wide; #1762 traced a wrong
|
|
43
|
-
* count to a shell snippet in `gsd-core/workflows/progress.md`.
|
|
44
|
-
* inward: the OWNER FILE (`src/state.cts`) is not exempt, only its
|
|
45
|
-
* named canonical FUNCTIONS are (`SEAM_OWNER_EXEMPT_FUNCTIONS` below).
|
|
46
|
-
* - 4(e) Axis 2 ships ratcheted because Phase 1 (this file) cannot
|
|
47
|
-
* consolidate the write seam — that is Phase 2 (#3469). Landing a
|
|
48
|
-
* guard later, against an already-clean tree, is the "found it, wrote
|
|
49
|
-
* it down, moved on" posture the epic removes.
|
|
103
|
+
* count to a shell snippet in `gsd-core/workflows/progress.md`.
|
|
50
104
|
*
|
|
51
|
-
* GOODHART, PER ADR-3408 DECISION 5: "0
|
|
105
|
+
* GOODHART, PER ADR-3408 DECISION 5: "0 violations" is a LAGGING metric — a
|
|
52
106
|
* measure about to become a target. This guard's own `_comment` and its
|
|
53
107
|
* human-readable success message both say so: the zero this guard reports
|
|
54
108
|
* must NEVER be quoted alone; it is only meaningful beside the behavioral
|
|
@@ -62,42 +116,37 @@
|
|
|
62
116
|
* to prevent. `stripComments` does not track quoted strings for exactly
|
|
63
117
|
* this reason — see its own header.
|
|
64
118
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* `
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* deliberately NOT full alias/dataflow tracking — see the function's own
|
|
90
|
-
* docstring for the narrow, documented limitation this trades for
|
|
91
|
-
* tractability.
|
|
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.
|
|
92
143
|
*/
|
|
93
144
|
|
|
94
|
-
const fs = require('node:fs');
|
|
95
145
|
const path = require('node:path');
|
|
96
146
|
const { scanTree, sanitizeForReport } = require('./lib/drift-scan.cjs');
|
|
97
147
|
const { escapeRegex } = require('../gsd-core/bin/lib/pattern.cjs');
|
|
98
148
|
|
|
99
149
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
100
|
-
const BASELINE_PATH = path.join(__dirname, 'state-write-path-drift-baseline.json');
|
|
101
150
|
|
|
102
151
|
// Frozen REASON enum — mirrors `lint-state-field-drift.cjs`'s house style of
|
|
103
152
|
// naming every failure shape explicitly rather than reusing one generic
|
|
@@ -110,44 +159,52 @@ const REASON = Object.freeze({
|
|
|
110
159
|
// with a variable field-name argument whose content argument was not run
|
|
111
160
|
// through `stripFrontmatter` first — see `findUnstrippedContentWrites`.
|
|
112
161
|
UNSTRIPPED_CONTENT_WRITE: 'unstripped_content_write',
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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',
|
|
118
172
|
});
|
|
119
173
|
|
|
120
174
|
// Scan surface — declared, per Decision 4(d), never inferred from `src/`
|
|
121
|
-
// alone. `src/` covers the executor
|
|
122
|
-
//
|
|
123
|
-
//
|
|
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.
|
|
124
178
|
const SRC_DIRS = ['src'];
|
|
125
179
|
const SRC_EXT = new Set(['.cts']);
|
|
126
180
|
const PROMPT_DIRS = ['gsd-core/workflows', 'commands', 'agents', 'skills'];
|
|
127
181
|
const PROMPT_EXT = new Set(['.md']);
|
|
128
182
|
|
|
129
|
-
// The executor (Axis 1
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
// `process.platform`.
|
|
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`.
|
|
133
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.
|
|
134
190
|
const SEAM_OWNER_FILE = 'src/state.cts';
|
|
135
191
|
|
|
136
192
|
// Per Decision 4(d)'s "owner FILE is not exempt, only its named canonical
|
|
137
|
-
// FUNCTIONS are": a `
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
// `
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
// in
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
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
|
|
151
208
|
// exists to catch.
|
|
152
209
|
const SEAM_OWNER_EXEMPT_FUNCTIONS = ['writeStateMd', 'syncAndPreserveStateMd'];
|
|
153
210
|
|
|
@@ -211,8 +268,9 @@ function stripComments(text) {
|
|
|
211
268
|
}
|
|
212
269
|
|
|
213
270
|
// A named function declaration, tolerating `export`/`async` prefixes — the
|
|
214
|
-
// SAME shape `
|
|
215
|
-
// uses to recognise (and skip) the seam
|
|
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.
|
|
216
274
|
const FUNCTION_DECL_LINE_RE = /^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/;
|
|
217
275
|
|
|
218
276
|
/**
|
|
@@ -323,8 +381,8 @@ function findPolicyDispatchDrift(rel, text) {
|
|
|
323
381
|
// `file` is sanitized here, at construction, not just at the human
|
|
324
382
|
// formatter: `rel` is exactly as attacker-controlled as `source` on a
|
|
325
383
|
// fork PR (a tracked filename can legally carry C1 bytes or bidi
|
|
326
|
-
// overrides), and it reaches
|
|
327
|
-
//
|
|
384
|
+
// overrides), and it reaches `--json` stdout unfiltered otherwise — see
|
|
385
|
+
// `sanitizeForReport`'s own header.
|
|
328
386
|
FIELD_NAME_DISPATCH_RE.lastIndex = 0;
|
|
329
387
|
let m;
|
|
330
388
|
while ((m = FIELD_NAME_DISPATCH_RE.exec(line)) !== null) {
|
|
@@ -336,7 +394,7 @@ function findPolicyDispatchDrift(rel, text) {
|
|
|
336
394
|
// `field` is captured straight out of a quoted string literal in
|
|
337
395
|
// repo source — attacker-controlled on the same fork-PR basis as
|
|
338
396
|
// `file`/`source`, so sanitize it too rather than let it reach
|
|
339
|
-
// `--json` stdout
|
|
397
|
+
// `--json` stdout.
|
|
340
398
|
field: sanitizeForReport(m[2]),
|
|
341
399
|
source: sanitizeForReport(line.trim()),
|
|
342
400
|
});
|
|
@@ -348,10 +406,6 @@ function findPolicyDispatchDrift(rel, text) {
|
|
|
348
406
|
axis: 'policy-dispatch',
|
|
349
407
|
file: sanitizeForReport(rel),
|
|
350
408
|
line: i + 1,
|
|
351
|
-
// `field` is captured straight out of a quoted string literal in
|
|
352
|
-
// repo source — attacker-controlled on the same fork-PR basis as
|
|
353
|
-
// `file`/`source`, so sanitize it too rather than let it reach
|
|
354
|
-
// `--json` stdout / the baseline raw.
|
|
355
409
|
field: sanitizeForReport(m[2]),
|
|
356
410
|
source: sanitizeForReport(line.trim()),
|
|
357
411
|
});
|
|
@@ -363,10 +417,6 @@ function findPolicyDispatchDrift(rel, text) {
|
|
|
363
417
|
axis: 'policy-dispatch',
|
|
364
418
|
file: sanitizeForReport(rel),
|
|
365
419
|
line: i + 1,
|
|
366
|
-
// `field` is captured straight out of a quoted string literal in
|
|
367
|
-
// repo source — attacker-controlled on the same fork-PR basis as
|
|
368
|
-
// `file`/`source`, so sanitize it too rather than let it reach
|
|
369
|
-
// `--json` stdout / the baseline raw.
|
|
370
420
|
field: sanitizeForReport(m[2]),
|
|
371
421
|
source: sanitizeForReport(line.trim()),
|
|
372
422
|
});
|
|
@@ -429,11 +479,9 @@ function isQuotedLiteralArg(arg) {
|
|
|
429
479
|
* The nearest assignment to `varName` (`varName = <expr>` or
|
|
430
480
|
* `const|let|var varName = <expr>`), scanning `lines` BACKWARD from `index`
|
|
431
481
|
* (inclusive) and stopping at the nearest preceding named-function
|
|
432
|
-
* declaration
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
* such assignment is found before the boundary — meaning `varName` is the
|
|
436
|
-
* enclosing function's own untouched parameter.
|
|
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.
|
|
437
485
|
*
|
|
438
486
|
* Deliberately single-hop: this reports whatever the NEAREST assignment's
|
|
439
487
|
* right-hand side literally is, and does not itself follow a further alias
|
|
@@ -506,31 +554,115 @@ function findUnstrippedContentWrites(rel, text) {
|
|
|
506
554
|
return out;
|
|
507
555
|
}
|
|
508
556
|
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
// `
|
|
512
|
-
// `
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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/;
|
|
523
656
|
|
|
524
657
|
/**
|
|
525
|
-
* AXIS
|
|
526
|
-
* `applyPostSyncPreservation(` call in `text`, outside the
|
|
527
|
-
*
|
|
528
|
-
* `SEAM_OWNER_EXEMPT_FUNCTIONS`'s own bodies.
|
|
529
|
-
*
|
|
530
|
-
* is
|
|
531
|
-
* knows about it.
|
|
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.
|
|
532
664
|
*/
|
|
533
|
-
function
|
|
665
|
+
function findCompositionBypasses(rel, text) {
|
|
534
666
|
const rawLines = text.split('\n');
|
|
535
667
|
const stripped = stripComments(text);
|
|
536
668
|
const out = [];
|
|
@@ -545,13 +677,11 @@ function findSeamBypasses(rel, text) {
|
|
|
545
677
|
const fn = enclosingFunction(stripped, i);
|
|
546
678
|
if (fn && SEAM_OWNER_EXEMPT_FUNCTIONS.includes(fn)) continue;
|
|
547
679
|
}
|
|
548
|
-
// `file`
|
|
549
|
-
//
|
|
550
|
-
// fork PR (a tracked filename can legally carry C1 bytes or bidi
|
|
551
|
-
// overrides), and it reaches the committed baseline and `--json`
|
|
552
|
-
// stdout unfiltered otherwise — see `sanitizeForReport`'s own header.
|
|
680
|
+
// `file`/`source` sanitized for the same fork-PR reason as every other
|
|
681
|
+
// finding in this guard.
|
|
553
682
|
out.push({
|
|
554
|
-
|
|
683
|
+
reason: REASON.COMPOSITION_BYPASS,
|
|
684
|
+
axis: 'composition-bypass',
|
|
555
685
|
file: sanitizeForReport(rel),
|
|
556
686
|
line: i + 1,
|
|
557
687
|
symbol: m[1],
|
|
@@ -600,10 +730,11 @@ function isInsideCodeSpan(line, index) {
|
|
|
600
730
|
}
|
|
601
731
|
|
|
602
732
|
/**
|
|
603
|
-
* AXIS
|
|
604
|
-
* write-side `gsd-tools` subcommand.
|
|
605
|
-
*
|
|
606
|
-
*
|
|
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.
|
|
607
738
|
*
|
|
608
739
|
* A candidate occurrence enclosed in backticks is a MENTION, not an
|
|
609
740
|
* invocation, and is deliberately not reported — CONTRIBUTING.md's "Every
|
|
@@ -614,8 +745,7 @@ function isInsideCodeSpan(line, index) {
|
|
|
614
745
|
* wrong: the first `lint-phase-enumeration-drift.cjs` flagged JSDoc that
|
|
615
746
|
* merely documented the canonical owner as drift, which trains readers to
|
|
616
747
|
* reflexively exempt documentation instead of trusting the guard — the
|
|
617
|
-
* opposite of Decision 4(a)'s intent.
|
|
618
|
-
* prompt-layer baseline entries were exactly this false positive.
|
|
748
|
+
* opposite of Decision 4(a)'s intent.
|
|
619
749
|
*/
|
|
620
750
|
function findPromptSeamUses(rel, text) {
|
|
621
751
|
const lines = text.split('\n');
|
|
@@ -626,10 +756,11 @@ function findPromptSeamUses(rel, text) {
|
|
|
626
756
|
let m;
|
|
627
757
|
while ((m = PROMPT_SEAM_RE.exec(line)) !== null) {
|
|
628
758
|
if (isInsideCodeSpan(line, m.index)) continue;
|
|
629
|
-
// `file` is sanitized here for the same reason as
|
|
630
|
-
//
|
|
631
|
-
// `source`.
|
|
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`.
|
|
632
762
|
out.push({
|
|
763
|
+
reason: REASON.PROMPT_LAYER_STATE_WRITE,
|
|
633
764
|
axis: 'write-seam',
|
|
634
765
|
file: sanitizeForReport(rel),
|
|
635
766
|
line: i + 1,
|
|
@@ -642,149 +773,23 @@ function findPromptSeamUses(rel, text) {
|
|
|
642
773
|
}
|
|
643
774
|
|
|
644
775
|
/**
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*/
|
|
650
|
-
function ratchetKey(v) {
|
|
651
|
-
return `${v.file} ${v.source}`;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* Read `BASELINE_PATH`. Returns `{ entries: [] }` when the file is ABSENT
|
|
656
|
-
* (`ENOENT` — first run, or a fully-shrunk Phase 4 baseline that deleted the
|
|
657
|
-
* file — ADR-3408 §8.3's roster foresees exactly this end state); returns
|
|
658
|
-
* `{ entries: null, code }` when the file is present but could not be read
|
|
659
|
-
* OR could not be parsed/shaped (missing/malformed `entries` array) — `code`
|
|
660
|
-
* carries the underlying `fs` error code (e.g. `'EACCES'`) when the failure
|
|
661
|
-
* happened at the read step, `null` when it happened at the parse/shape
|
|
662
|
-
* step, so the caller can fail closed rather than silently ratcheting
|
|
663
|
-
* against nothing. Returns the parsed object when the read+parse succeed.
|
|
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.
|
|
664
780
|
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
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`).
|
|
673
789
|
*/
|
|
674
|
-
function
|
|
675
|
-
let raw;
|
|
676
|
-
try {
|
|
677
|
-
raw = fs.readFileSync(BASELINE_PATH, 'utf8');
|
|
678
|
-
} catch (err) {
|
|
679
|
-
if (err && err.code === 'ENOENT') return { entries: [] };
|
|
680
|
-
return { entries: null, code: err && err.code ? err.code : 'UNKNOWN' };
|
|
681
|
-
}
|
|
682
|
-
let doc;
|
|
683
|
-
try {
|
|
684
|
-
doc = JSON.parse(raw);
|
|
685
|
-
} catch {
|
|
686
|
-
return { entries: null, code: null };
|
|
687
|
-
}
|
|
688
|
-
if (!doc || typeof doc !== 'object' || !Array.isArray(doc.entries)) return { entries: null, code: null };
|
|
689
|
-
return doc;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/**
|
|
693
|
-
* The ratchet — mirrors `scripts/qa-smell-ratchet.cjs`'s four invariants,
|
|
694
|
-
* applied to write-seam bypass COUNTS instead of QA-smell fingerprints:
|
|
695
|
-
*
|
|
696
|
-
* 1. An observed key absent from the baseline is a brand-new,
|
|
697
|
-
* unacknowledged bypass — `SEAM_BYPASS_UNRECORDED`.
|
|
698
|
-
* 2. An observed count greater than the acknowledged count is a NEW copy
|
|
699
|
-
* landing beside an already-acknowledged one — `SEAM_BYPASS_COUNT_GREW`.
|
|
700
|
-
* 3. An observed count less than the acknowledged count is a PARTIAL
|
|
701
|
-
* migration — some but not all call sites at this exact key were
|
|
702
|
-
* removed, and the baseline still claims the old, larger number —
|
|
703
|
-
* `SEAM_BYPASS_COUNT_SHRANK`.
|
|
704
|
-
* 4. A baseline key with zero current observations is a STALE
|
|
705
|
-
* acknowledgment: an entry may never outlive what it describes, and
|
|
706
|
-
* the baseline may only shrink (via `--baseline`, regenerated) —
|
|
707
|
-
* `BASELINE_ENTRY_STALE`.
|
|
708
|
-
*
|
|
709
|
-
* The occurrence COUNT (not just key presence) is what makes a partial
|
|
710
|
-
* migration visible at all: two byte-identical call sites in one file are
|
|
711
|
-
* otherwise a single indistinguishable key, so removing one of the two
|
|
712
|
-
* would silently vanish from a presence-only check while the acknowledgment
|
|
713
|
-
* still describes two.
|
|
714
|
-
*
|
|
715
|
-
* Every returned finding carries both `observed` and `acknowledged` counts.
|
|
716
|
-
*/
|
|
717
|
-
function applyRatchet(observed, baseline) {
|
|
718
|
-
const observedByKey = new Map();
|
|
719
|
-
for (const finding of observed) {
|
|
720
|
-
const key = ratchetKey(finding);
|
|
721
|
-
let group = observedByKey.get(key);
|
|
722
|
-
if (!group) {
|
|
723
|
-
group = { count: 0, sample: finding };
|
|
724
|
-
observedByKey.set(key, group);
|
|
725
|
-
}
|
|
726
|
-
group.count += 1;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
const baselineByKey = new Map();
|
|
730
|
-
for (const entry of baseline.entries) {
|
|
731
|
-
baselineByKey.set(`${entry.file} ${entry.source}`, entry);
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
const out = [];
|
|
735
|
-
for (const [key, group] of observedByKey) {
|
|
736
|
-
const entry = baselineByKey.get(key);
|
|
737
|
-
const acknowledged = entry && typeof entry.count === 'number' ? entry.count : 0;
|
|
738
|
-
let reason = null;
|
|
739
|
-
if (!entry) {
|
|
740
|
-
reason = REASON.SEAM_BYPASS_UNRECORDED;
|
|
741
|
-
} else if (group.count > acknowledged) {
|
|
742
|
-
reason = REASON.SEAM_BYPASS_COUNT_GREW;
|
|
743
|
-
} else if (group.count < acknowledged) {
|
|
744
|
-
reason = REASON.SEAM_BYPASS_COUNT_SHRANK;
|
|
745
|
-
}
|
|
746
|
-
if (!reason) continue;
|
|
747
|
-
out.push({
|
|
748
|
-
reason,
|
|
749
|
-
axis: 'write-seam',
|
|
750
|
-
file: group.sample.file,
|
|
751
|
-
line: group.sample.line,
|
|
752
|
-
symbol: group.sample.symbol,
|
|
753
|
-
source: group.sample.source,
|
|
754
|
-
observed: group.count,
|
|
755
|
-
acknowledged,
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
for (const [key, entry] of baselineByKey) {
|
|
760
|
-
if (observedByKey.has(key)) continue;
|
|
761
|
-
out.push({
|
|
762
|
-
reason: REASON.BASELINE_ENTRY_STALE,
|
|
763
|
-
axis: 'write-seam',
|
|
764
|
-
file: entry.file,
|
|
765
|
-
line: 0,
|
|
766
|
-
symbol: entry.symbol,
|
|
767
|
-
source: entry.source,
|
|
768
|
-
observed: 0,
|
|
769
|
-
acknowledged: typeof entry.count === 'number' ? entry.count : 0,
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
return out;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
/**
|
|
777
|
-
* Run both scan passes (the `src/` tree for Axis 1 + Axis 2a + Axis 3, the
|
|
778
|
-
* prompt layer for Axis 2b) and split the combined findings by `axis` into
|
|
779
|
-
* `{ policyFindings, seamFindings }`. `policyFindings` are already terminal
|
|
780
|
-
* (each carries its own `reason`) — this bucket is every axis EXCEPT
|
|
781
|
-
* `write-seam` (Axis 2), which alone is ratcheted; `seamFindings` are raw
|
|
782
|
-
* write-seam observations — `applyRatchet` is what turns them into (or
|
|
783
|
-
* clears them of) a finding.
|
|
784
|
-
*/
|
|
785
|
-
function collect() {
|
|
790
|
+
function collect(root = REPO_ROOT) {
|
|
786
791
|
const srcFindings = scanTree({
|
|
787
|
-
root
|
|
792
|
+
root,
|
|
788
793
|
scanDirs: SRC_DIRS,
|
|
789
794
|
scanExt: SRC_EXT,
|
|
790
795
|
onFile(rel, text) {
|
|
@@ -795,13 +800,14 @@ function collect() {
|
|
|
795
800
|
found.push(...findUnimplementedPolicies(text, relPosix));
|
|
796
801
|
found.push(...findUnstrippedContentWrites(relPosix, text));
|
|
797
802
|
}
|
|
798
|
-
found.push(...
|
|
803
|
+
found.push(...findRawStateWrites(relPosix, text));
|
|
804
|
+
found.push(...findCompositionBypasses(relPosix, text));
|
|
799
805
|
return found;
|
|
800
806
|
},
|
|
801
807
|
});
|
|
802
808
|
|
|
803
809
|
const promptFindings = scanTree({
|
|
804
|
-
root
|
|
810
|
+
root,
|
|
805
811
|
scanDirs: PROMPT_DIRS,
|
|
806
812
|
scanExt: PROMPT_EXT,
|
|
807
813
|
onFile(rel, text) {
|
|
@@ -809,96 +815,11 @@ function collect() {
|
|
|
809
815
|
},
|
|
810
816
|
});
|
|
811
817
|
|
|
812
|
-
|
|
813
|
-
return {
|
|
814
|
-
policyFindings: all.filter((f) => f.axis !== 'write-seam'),
|
|
815
|
-
seamFindings: all.filter((f) => f.axis === 'write-seam'),
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
/**
|
|
820
|
-
* Group `seamFindings` by `ratchetKey` into the baseline entry shape
|
|
821
|
-
* (`{file, source, symbol, count, owner}`), sorted by `file+source`.
|
|
822
|
-
*
|
|
823
|
-
* `owner` is NEVER invented by this mechanical regeneration — the guard can
|
|
824
|
-
* observe WHERE a bypass is and HOW MANY there are, but not which issue owns
|
|
825
|
-
* removing it; inventing one would violate the same "never guess" discipline
|
|
826
|
-
* `qa-smell-ratchet.cjs` applies to its own `issue` field (its `--update`
|
|
827
|
-
* never invents an issue number either). ADR-3408 §8.3 requires every
|
|
828
|
-
* shipped entry to carry "a named ratchet entry carrying the issue that owns
|
|
829
|
-
* its removal, never an unrecorded pass" — that owner is HUMAN-CURATED and
|
|
830
|
-
* must be recorded before the entry ships.
|
|
831
|
-
*
|
|
832
|
-
* Because `--baseline` overwrites `BASELINE_PATH` wholesale, a naive
|
|
833
|
-
* mechanical regeneration would silently re-null every curated `owner` on
|
|
834
|
-
* each run. To avoid that, `existingEntries` (the baseline as it stood
|
|
835
|
-
* BEFORE this regeneration, i.e. `loadBaseline().entries`) is optional and,
|
|
836
|
-
* when supplied, its `owner` values are merged forward onto matching new
|
|
837
|
-
* entries keyed on `(file, source)` — the same key `ratchetKey` /
|
|
838
|
-
* `applyRatchet` use to identify a bypass. A key with no prior entry (a
|
|
839
|
-
* brand-new bypass) still gets `owner: null`, exactly as before; only
|
|
840
|
-
* already-curated owners survive the regeneration. Re-running `--baseline`
|
|
841
|
-
* twice in a row is therefore idempotent with respect to `owner`.
|
|
842
|
-
*/
|
|
843
|
-
function buildBaselineEntries(seamFindings, existingEntries) {
|
|
844
|
-
const priorOwnerByKey = new Map();
|
|
845
|
-
if (Array.isArray(existingEntries)) {
|
|
846
|
-
for (const entry of existingEntries) {
|
|
847
|
-
priorOwnerByKey.set(ratchetKey(entry), entry.owner);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
const groups = new Map();
|
|
852
|
-
for (const finding of seamFindings) {
|
|
853
|
-
const key = ratchetKey(finding);
|
|
854
|
-
let group = groups.get(key);
|
|
855
|
-
if (!group) {
|
|
856
|
-
group = {
|
|
857
|
-
file: finding.file,
|
|
858
|
-
source: finding.source,
|
|
859
|
-
symbol: finding.symbol,
|
|
860
|
-
count: 0,
|
|
861
|
-
owner: priorOwnerByKey.has(key) ? priorOwnerByKey.get(key) : null,
|
|
862
|
-
};
|
|
863
|
-
groups.set(key, group);
|
|
864
|
-
}
|
|
865
|
-
group.count += 1;
|
|
866
|
-
}
|
|
867
|
-
return [...groups.values()].sort((a, b) => ratchetKey(a).localeCompare(ratchetKey(b)));
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
const BASELINE_COMMENT =
|
|
871
|
-
'ADR-3408 Decision 5 write-seam ratchet baseline (issue #3468, Phase 1; Phase 2 / #3469 lands the ' +
|
|
872
|
-
'single write seam and Amendment 2). Every entry here is a `writeStateMd(`/`syncStateFrontmatter(`/' +
|
|
873
|
-
'`applyPostSyncPreservation(` bypass this guard found by a whole-repo scan (Decision 4(a)) — it is ' +
|
|
874
|
-
'ACKNOWLEDGED, not endorsed: acknowledgment is in writing (this file), with the issue owning its ' +
|
|
875
|
-
'removal recorded in the entry\'s "owner" field. This baseline is SHRINK-ONLY — an entry that stops ' +
|
|
876
|
-
'firing goes STALE and fails the plain run until `--baseline` is re-run to drop it (ADR-3180 ' +
|
|
877
|
-
'Decision 4(e)\'s "the baseline may only shrink", adopted verbatim by ADR-3408). Phase 2 (#3469) ' +
|
|
878
|
-
'removed the `cmdPhaseComplete` (`src/phase.cts`) and `cmdMilestoneComplete` (`src/milestone.cts`) ' +
|
|
879
|
-
'entries by routing both through the single write-seam composition (`syncAndPreserveStateMd`, ' +
|
|
880
|
-
'`src/state.cts`). ADR-3408 Amendment 2: "0 bypasses" was never this baseline\'s target — TWO ' +
|
|
881
|
-
'entries are SANCTIONED PERMANENT, not debt, and Phase 4 (#3471) does NOT drive this file to empty: ' +
|
|
882
|
-
'`cmdStateSync` (`src/state.cts`) exists precisely to let the body win (#905 — `state sync` ' +
|
|
883
|
-
're-derives frontmatter FROM the body), so routing it through preservation would invert the command ' +
|
|
884
|
-
'rather than fix a bug; `REGENERATE_STATE` (`src/health-diagnostic.cts`) is `/gsd-health --repair`\'s ' +
|
|
885
|
-
'factory reset, which rebuilds STATE.md from scratch, so preservation would restore exactly the ' +
|
|
886
|
-
'values it was invoked to discard. Neither entry may be "consolidated" away — a guard reporting them ' +
|
|
887
|
-
'is reporting correctly, and a change that removes one is a regression, not progress.';
|
|
888
|
-
|
|
889
|
-
function writeBaseline(seamFindings) {
|
|
890
|
-
const priorBaseline = loadBaseline();
|
|
891
|
-
const entries = buildBaselineEntries(
|
|
892
|
-
seamFindings,
|
|
893
|
-
Array.isArray(priorBaseline.entries) ? priorBaseline.entries : null,
|
|
894
|
-
);
|
|
895
|
-
const doc = { _comment: BASELINE_COMMENT, entries };
|
|
896
|
-
fs.writeFileSync(BASELINE_PATH, `${JSON.stringify(doc, null, 2)}\n`, 'utf8');
|
|
897
|
-
return entries;
|
|
818
|
+
return { findings: [...srcFindings, ...promptFindings] };
|
|
898
819
|
}
|
|
899
820
|
|
|
900
821
|
const GOODHART_NOTE =
|
|
901
|
-
'Goodhart note (ADR-3408 Decision 5): this "0 write-path
|
|
822
|
+
'Goodhart note (ADR-3408 Decision 5): this "0 write-path violations" is a LAGGING metric — report ' +
|
|
902
823
|
'it only alongside the behavioral identity test\'s result (asserted at the consumer\'s output), ' +
|
|
903
824
|
'never alone.';
|
|
904
825
|
|
|
@@ -910,81 +831,66 @@ function printFindings(findings) {
|
|
|
910
831
|
}
|
|
911
832
|
|
|
912
833
|
/**
|
|
913
|
-
* `argv
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
* baseline), 2 usage error.
|
|
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.
|
|
918
838
|
*/
|
|
919
|
-
function
|
|
839
|
+
function parseArgs(argv) {
|
|
920
840
|
const args = argv || [];
|
|
921
|
-
|
|
922
|
-
|
|
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;
|
|
923
878
|
if (unknown.length > 0) {
|
|
924
879
|
process.stderr.write(
|
|
925
880
|
`lint-state-write-path-drift: unrecognized argument(s): ${unknown.map((a) => sanitizeForReport(a)).join(', ')} ` +
|
|
926
|
-
'(expected --
|
|
881
|
+
'(expected --json and/or --root <dir>)\n',
|
|
927
882
|
);
|
|
928
883
|
process.exitCode = 2;
|
|
929
884
|
return;
|
|
930
885
|
}
|
|
931
886
|
|
|
932
|
-
|
|
933
|
-
const { seamFindings } = collect();
|
|
934
|
-
const entries = writeBaseline(seamFindings);
|
|
935
|
-
process.stdout.write(`lint-state-write-path-drift: wrote ${entries.length} entries to ${BASELINE_PATH}\n`);
|
|
936
|
-
process.exitCode = 0;
|
|
937
|
-
return;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
const wantJson = args.includes('--json');
|
|
941
|
-
const baseline = loadBaseline();
|
|
942
|
-
|
|
943
|
-
if (baseline.entries === null) {
|
|
944
|
-
const finding = {
|
|
945
|
-
reason: REASON.BASELINE_UNREADABLE,
|
|
946
|
-
axis: 'write-seam',
|
|
947
|
-
file: path.relative(REPO_ROOT, BASELINE_PATH),
|
|
948
|
-
line: 0,
|
|
949
|
-
symbol: null,
|
|
950
|
-
code: baseline.code,
|
|
951
|
-
source: sanitizeForReport(
|
|
952
|
-
baseline.code
|
|
953
|
-
? `${BASELINE_PATH} is present but could not be read (${baseline.code}) — run \`node ${__filename} --baseline\` to regenerate it`
|
|
954
|
-
: `${BASELINE_PATH} is present but unparseable — run \`node ${__filename} --baseline\` to regenerate it`,
|
|
955
|
-
),
|
|
956
|
-
};
|
|
957
|
-
if (wantJson) {
|
|
958
|
-
process.stdout.write(
|
|
959
|
-
`${JSON.stringify(
|
|
960
|
-
{
|
|
961
|
-
ok: false,
|
|
962
|
-
findings: [finding],
|
|
963
|
-
summary: { policyDispatchViolations: 0, seamBypassesObserved: 0, seamBypassesAcknowledged: 0, ratchetViolations: 0 },
|
|
964
|
-
},
|
|
965
|
-
null,
|
|
966
|
-
2,
|
|
967
|
-
)}\n`,
|
|
968
|
-
);
|
|
969
|
-
} else {
|
|
970
|
-
printFindings([finding]);
|
|
971
|
-
}
|
|
972
|
-
process.exitCode = 1;
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
const { policyFindings, seamFindings } = collect();
|
|
977
|
-
const ratchetFindings = applyRatchet(seamFindings, baseline);
|
|
978
|
-
const findings = [...policyFindings, ...ratchetFindings];
|
|
979
|
-
const acknowledgedTotal = baseline.entries.reduce(
|
|
980
|
-
(sum, e) => sum + (typeof e.count === 'number' ? e.count : 0),
|
|
981
|
-
0,
|
|
982
|
-
);
|
|
887
|
+
const { findings } = collect(root);
|
|
983
888
|
const summary = {
|
|
984
|
-
policyDispatchViolations:
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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,
|
|
988
894
|
};
|
|
989
895
|
|
|
990
896
|
if (wantJson) {
|
|
@@ -995,8 +901,8 @@ function main(argv) {
|
|
|
995
901
|
|
|
996
902
|
if (findings.length === 0) {
|
|
997
903
|
process.stdout.write(
|
|
998
|
-
'ok state-write-path-drift: no policy-dispatch
|
|
999
|
-
'
|
|
904
|
+
'ok state-write-path-drift: no policy-dispatch, frontmatter-write, raw-write, prompt-layer, or ' +
|
|
905
|
+
'composition-bypass violations found\n',
|
|
1000
906
|
);
|
|
1001
907
|
process.stdout.write(`${GOODHART_NOTE}\n`);
|
|
1002
908
|
process.exitCode = 0;
|
|
@@ -1004,8 +910,9 @@ function main(argv) {
|
|
|
1004
910
|
}
|
|
1005
911
|
|
|
1006
912
|
process.stderr.write(
|
|
1007
|
-
'state-write-path-drift:
|
|
1008
|
-
'
|
|
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',
|
|
1009
916
|
);
|
|
1010
917
|
printFindings(findings);
|
|
1011
918
|
process.exitCode = 1;
|
|
@@ -1016,7 +923,6 @@ if (require.main === module) main(process.argv.slice(2));
|
|
|
1016
923
|
module.exports = {
|
|
1017
924
|
REASON,
|
|
1018
925
|
REPO_ROOT,
|
|
1019
|
-
BASELINE_PATH,
|
|
1020
926
|
SRC_DIRS,
|
|
1021
927
|
SRC_EXT,
|
|
1022
928
|
PROMPT_DIRS,
|
|
@@ -1033,13 +939,12 @@ module.exports = {
|
|
|
1033
939
|
findUnstrippedContentWrites,
|
|
1034
940
|
isQuotedLiteralArg,
|
|
1035
941
|
nearestPrecedingAssignment,
|
|
1036
|
-
|
|
942
|
+
findRawStateWrites,
|
|
943
|
+
targetsStatePath,
|
|
944
|
+
findCompositionBypasses,
|
|
1037
945
|
findPromptSeamUses,
|
|
1038
946
|
isInsideCodeSpan,
|
|
1039
|
-
|
|
1040
|
-
loadBaseline,
|
|
1041
|
-
applyRatchet,
|
|
947
|
+
parseArgs,
|
|
1042
948
|
collect,
|
|
1043
|
-
buildBaselineEntries,
|
|
1044
949
|
main,
|
|
1045
950
|
};
|