@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,805 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Anti-divergence drift guard for the STATE.md FIELD-EXTRACTION FALLBACK
|
|
6
|
+
* CHAIN (epic #3180, issue #3187, ADR-3180 Decision 4, spec §7.7).
|
|
7
|
+
*
|
|
8
|
+
* The derivation this guard protects is "read a STATE.md field: prefer the
|
|
9
|
+
* YAML frontmatter scalar (string, trimmed-non-empty; or number/boolean
|
|
10
|
+
* coerced to a string), else fall back to the body field via
|
|
11
|
+
* `stateExtractField(body, bodyField)`". `src/state-document.cts` is
|
|
12
|
+
* DESIGNATED the single canonical owner of this grammar (issue #3187 Phase
|
|
13
|
+
* 5), but as of this guard's authorship (Phase 5's guard-first step, per
|
|
14
|
+
* ADR-3180 Amendment 3's standing rule) the canonical function does not yet
|
|
15
|
+
* exist there — this script is written to discover every existing
|
|
16
|
+
* re-derivation BEFORE any src/ file is touched, exactly the ordering
|
|
17
|
+
* `lint-plan-count-drift.cjs` and its siblings established.
|
|
18
|
+
*
|
|
19
|
+
* Per ADR-3180 Decision 4(a) this guard discovers call sites by SCANNING THE
|
|
20
|
+
* WHOLE `src/` TREE, not by consulting an allowlist of known files. Per
|
|
21
|
+
* Decision 4(d) that surface is widened further still: `src/` alone is
|
|
22
|
+
* itself the forbidden allowlist, one directory wide, so this guard ALSO
|
|
23
|
+
* scans the prompt-layer markdown (`gsd-core/workflows`, `commands`,
|
|
24
|
+
* `agents`, `skills`) for a PROSE re-derivation of the same chain — see the
|
|
25
|
+
* "PROMPT-LAYER PROSE DETECTION" section below `findStateFieldDrift`.
|
|
26
|
+
*
|
|
27
|
+
* DETECTION: FUNCTION-SCOPED CO-OCCURRENCE, not a bounded line-window.
|
|
28
|
+
* An earlier draft of this guard used a fixed line-distance window between
|
|
29
|
+
* the ladder and the fallback call. That shape was REJECTED: it produced a
|
|
30
|
+
* documented "known miss" on live copies inside the very function it was
|
|
31
|
+
* scanning (state.cts `cmdStateSnapshot`'s `Current Plan` / `Total Plans in
|
|
32
|
+
* Phase` / `Status` / `Progress` / `Last Activity` reads all sit further
|
|
33
|
+
* from the ladder than any defensible line count), which means the guard
|
|
34
|
+
* could report "0 re-derivations" after only the nearest few copies were
|
|
35
|
+
* migrated while several more of the SAME kind survived untouched in the
|
|
36
|
+
* SAME function — "a zero it did not earn" (ADR-3180 Decision 4(a)). A fixed
|
|
37
|
+
* N is also trivially gameable by reflow (Goodhart's law): moving a call one
|
|
38
|
+
* line further from its ladder silences the guard without changing the
|
|
39
|
+
* derivation at all. The window is dropped entirely; there is no magic
|
|
40
|
+
* number anywhere in this detection.
|
|
41
|
+
*
|
|
42
|
+
* The invariant instead: a NAMED FUNCTION that BOTH (a) contains a
|
|
43
|
+
* "frontmatter scalar coercion ladder" — the SAME operand compared via
|
|
44
|
+
* `typeof OPERAND === 'number'` and `typeof OPERAND === 'boolean'`
|
|
45
|
+
* (TYPEOF_TIER_CLAUSE_RE, order-independent, member/computed operands
|
|
46
|
+
* included, an optional `typeof OPERAND === 'string'` tier tolerated
|
|
47
|
+
* anywhere among them — see the ternary shape in `cmdStatePrune` below),
|
|
48
|
+
* within LADDER_WINDOW_LINES of each other — ANYWHERE in its own body,
|
|
49
|
+
* INCLUDING inside a nested closure it defines, AND (b) calls
|
|
50
|
+
* `stateExtractField(` anywhere in that same body, is re-deriving the
|
|
51
|
+
* fm-else-body fallback chain. EVERY `stateExtractField(` call line inside
|
|
52
|
+
* such a function is reported, not just the first.
|
|
53
|
+
*
|
|
54
|
+
* LADDER DETECTION, evasion-resistant shapes (see "LADDER DETECTION DETAIL"
|
|
55
|
+
* below for the full account, including the one shape still NOT caught):
|
|
56
|
+
* - operand may be a bare identifier (`v`), a dotted member expression
|
|
57
|
+
* (`fm.key`), or a computed access (`fm[key]`) — matched via
|
|
58
|
+
* OPERAND_SOURCE and compared as captured TEXT, not a bare-identifier
|
|
59
|
+
* backreference.
|
|
60
|
+
* - the two required tiers ('number', 'boolean') may appear in EITHER
|
|
61
|
+
* order; an optional third 'string' tier may sit anywhere among them.
|
|
62
|
+
* - the two clauses may sit on the SAME line (the classic single-line
|
|
63
|
+
* `||` chain) or on DIFFERENT lines within LADDER_WINDOW_LINES of each
|
|
64
|
+
* other — including as two entirely separate `if` statements, not just
|
|
65
|
+
* one expression wrapped across a line break.
|
|
66
|
+
*
|
|
67
|
+
* FUNCTION ATTRIBUTION. Two declaration shapes are recognised as opening a
|
|
68
|
+
* new named function scope:
|
|
69
|
+
* - `function NAME(...) {` (FUNCTION_DECL_RE) — top-level OR nested, at
|
|
70
|
+
* any indentation. `src/smart-entry.cts`'s `fmScalar` (line 144) is this
|
|
71
|
+
* shape, and is itself top-level.
|
|
72
|
+
* - `const NAME = (...): ReturnType => {` (ARROW_CONST_RE) — an arrow
|
|
73
|
+
* function assigned to a `const`, with a block body (`=> {`, not an
|
|
74
|
+
* expression body like `=> ({...})`, which never opens a new function
|
|
75
|
+
* frame — its `{` is an object literal, still counted toward brace
|
|
76
|
+
* depth, but attributes no name). `src/state.cts`'s `cmdStateSnapshot`
|
|
77
|
+
* defines its `fmScalar` (line 1464) this way, NESTED inside
|
|
78
|
+
* `cmdStateSnapshot` itself.
|
|
79
|
+
* `ARROW_CONST_RE`'s pattern requires `=>\s*\{` literally, so it only ever
|
|
80
|
+
* matches a line that already carries its opening brace — pushed
|
|
81
|
+
* immediately. `FUNCTION_DECL_RE` carries no such guarantee: a multi-line
|
|
82
|
+
* signature (parameters and/or a return-type annotation spilling onto later
|
|
83
|
+
* lines) matches on a line with NO `{` at all. An earlier version of this
|
|
84
|
+
* guard pushed such a match immediately anyway, recording `openDepth` at the
|
|
85
|
+
* ENCLOSING scope's depth rather than the function's own — for a top-level
|
|
86
|
+
* function, `openDepth: 0`, and because real code never reaches negative
|
|
87
|
+
* depth, a frame pushed with `openDepth: 0` could NEVER pop, sitting at the
|
|
88
|
+
* bottom of the stack for the rest of the file and silently misattributing
|
|
89
|
+
* every later line with no OTHER open frame to it. `src/state.cts`'s
|
|
90
|
+
* `preferNewerLastActivity` (a 4-line signature, `{` on its own line) is the
|
|
91
|
+
* live instance that surfaced this; it caused no observed false violation
|
|
92
|
+
* only because nothing ever called `stateExtractField(` at true module scope
|
|
93
|
+
* after it, not because the tracking was sound. `buildFunctionInfo` now
|
|
94
|
+
* DEFERS a `FUNCTION_DECL_RE` match (`pendingDeclName`) across lines until
|
|
95
|
+
* the first subsequent line whose brace count actually increases, and pushes
|
|
96
|
+
* the frame THERE, with `openDepth` computed from that line's `depth` —
|
|
97
|
+
* matching every other frame's push convention. A pending name is abandoned
|
|
98
|
+
* (never pushed) if a `;` terminates the statement before any `{` appears —
|
|
99
|
+
* a type-only declaration, `declare function`, or overload signature, none
|
|
100
|
+
* of which open a body.
|
|
101
|
+
* Function scopes NEST via a brace-depth stack: entering either shape pushes
|
|
102
|
+
* a frame; the frame pops once brace depth returns below the depth recorded
|
|
103
|
+
* when it was pushed. A line is attributed to the INNERMOST currently-open
|
|
104
|
+
* named frame (falling back to whatever enclosing frame IS open — typically
|
|
105
|
+
* the nearest enclosing top-level function — when a line sits between two
|
|
106
|
+
* sibling nested scopes; module-level code with no open frame is
|
|
107
|
+
* unattributed and therefore never a violation, matching "if you cannot
|
|
108
|
+
* identify one" in the design brief).
|
|
109
|
+
*
|
|
110
|
+
* TRANSITIVE ladder attribution is what makes `cmdStateSnapshot` (whose
|
|
111
|
+
* OWN top-level statements never spell the ladder themselves — only its
|
|
112
|
+
* nested `fmScalar` closure does) still register as ladder-bearing: when
|
|
113
|
+
* LADDER_RE matches a line, EVERY frame currently open on the stack at that
|
|
114
|
+
* point — not just the innermost — is marked ladder-bearing, because a
|
|
115
|
+
* nested closure's body is lexically part of every one of its enclosing
|
|
116
|
+
* functions' own bodies. `stateExtractField(` calls are attributed to the
|
|
117
|
+
* INNERMOST frame only (no transitivity needed there: the call already sits
|
|
118
|
+
* directly inside whichever frame is innermost at that point).
|
|
119
|
+
*
|
|
120
|
+
* BRACE-DEPTH COUNTING runs over `scanCode`'s per-line, cross-file output
|
|
121
|
+
* (see that function's own header for the full rationale and the concrete
|
|
122
|
+
* bug its cross-line comment/template tracking fixes) — comments and
|
|
123
|
+
* quoted/backtick string and template literal CONTENTS are already removed
|
|
124
|
+
* before a single brace is counted, escape-aware and threaded across line
|
|
125
|
+
* boundaries, so neither a brace inside a string (`{ label: '{' }`) nor one
|
|
126
|
+
* inside a multi-line block comment or template literal perturbs the depth
|
|
127
|
+
* count. It does NOT specially recognise regex literals (a `{` inside a
|
|
128
|
+
* `/.../ ` quantifier, e.g. `/x{2,3}/`, is counted as a plain character);
|
|
129
|
+
* see `scanCode`'s header for why every such literal actually present in
|
|
130
|
+
* `src/` today is harmless (balanced on its own line).
|
|
131
|
+
*
|
|
132
|
+
* MUST-NOT-FLAG case verified by running the guard (the earlier "case
|
|
133
|
+
* variant chain" exemption for `src/state.cts:1488` was WRONG and is
|
|
134
|
+
* SUPERSEDED — see the header of the guard's initial version in git history
|
|
135
|
+
* for the retracted reasoning; `cmdStateSnapshot` is ladder-bearing, so
|
|
136
|
+
* *every* `stateExtractField(` call inside it, including line 1488, is
|
|
137
|
+
* correctly a violation now):
|
|
138
|
+
* - `src/smart-entry.cts`'s `fmScalarKey` (lines 152-158): its own
|
|
139
|
+
* `typeof v === 'number' || typeof v === 'boolean'` ladder (line 156)
|
|
140
|
+
* reads a value out of a NESTED frontmatter object and never calls
|
|
141
|
+
* `stateExtractField(` anywhere in its own body — it is ladder-bearing
|
|
142
|
+
* but call-free, so it is correctly never flagged. This is the live
|
|
143
|
+
* control case proving the guard still distinguishes "has a ladder" from
|
|
144
|
+
* "re-derives the fallback chain": a ladder alone, with no body
|
|
145
|
+
* fallback call in the same function, is a different question (reading
|
|
146
|
+
* a nested frontmatter object, full stop) and stays silent.
|
|
147
|
+
* - Any ladder or `stateExtractField(` call appearing inside a `//` line
|
|
148
|
+
* comment or a `/* *\/`-style block comment (single- or multi-line).
|
|
149
|
+
* `scanCode` blanks comment text — cross-line-aware — before either
|
|
150
|
+
* regex runs, so prose describing this derivation is never mistaken for
|
|
151
|
+
* a copy of it (ADR-3180 Amendment 3's "trains readers to reflexively
|
|
152
|
+
* exempt documentation" note).
|
|
153
|
+
*
|
|
154
|
+
* FUNCTION-SCOPED EXEMPTIONS (per ADR-3180 Decision 4(a): NEVER a bare
|
|
155
|
+
* whole-file allowlist — Decision 4(d) records that a whole-file exemption
|
|
156
|
+
* on the owner is precisely how `getMilestoneInfo` stayed invisible to an
|
|
157
|
+
* earlier guard). `src/state-document.cts` is the owner of this grammar;
|
|
158
|
+
* its `stateFieldValue` (added for issue #3187 Phase 5, landed in this same
|
|
159
|
+
* working tree while this guard was being authored — see the guard's commit
|
|
160
|
+
* history / PR for the exact sequencing) IS the canonical
|
|
161
|
+
* frontmatter-scalar-then-body-field chain, not a copy of it, so it is the
|
|
162
|
+
* ONLY entry in FUNCTION_SCOPED_EXEMPTIONS. Every other function in
|
|
163
|
+
* `state-document.cts` — including any future re-derivation added anywhere
|
|
164
|
+
* else in that file — is still scanned and still flagged (mirrors
|
|
165
|
+
* `lint-completion-ratio-drift.cjs`'s `FUNCTION_SCOPED_EXEMPTIONS` for
|
|
166
|
+
* `clampPercent`/`clampPercentFromFraction`).
|
|
167
|
+
*
|
|
168
|
+
* Every regex below is small, bounded, and has no nested/overlapping
|
|
169
|
+
* quantifiers; the string-literal and brace scans are plain escape-aware
|
|
170
|
+
* character loops, not regexes, so there is nothing for a backtracking
|
|
171
|
+
* engine to explore. `npm run lint:ci` runs CodeQL js/redos over this repo;
|
|
172
|
+
* mirrors the ReDoS discipline of the sibling drift guards.
|
|
173
|
+
*
|
|
174
|
+
* The tree-walk / root-confinement / sanitizer machinery is SHARED with the
|
|
175
|
+
* sibling drift guards via `scripts/lib/drift-scan.cjs` (ADR-3180 Decision
|
|
176
|
+
* 4). This guard's detection shape needs no regex-literal extraction, so it
|
|
177
|
+
* does not use `readRegexLiteralAt`; the reported fragment is simply the
|
|
178
|
+
* trimmed source line, bounded to MAX_REGEX_LITERAL_LEN characters.
|
|
179
|
+
*
|
|
180
|
+
* LADDER DETECTION DETAIL (evasion history). An earlier version of this
|
|
181
|
+
* guard's ladder pattern was a single regex, `typeof (\w+) === 'number' \|\|
|
|
182
|
+
* typeof \1 === 'boolean'`, tested against ONE line. An isolated adversarial
|
|
183
|
+
* review found three live-shaped ways past it, all verified to produce ZERO
|
|
184
|
+
* violations against a real re-derivation:
|
|
185
|
+
* (a) a member-expression or computed operand — `typeof fm.key ===
|
|
186
|
+
* 'number' || typeof fm.key === 'boolean'` — never matched, because
|
|
187
|
+
* the bare-identifier backreference (`\w+` then `\1`) cannot match
|
|
188
|
+
* `fm.key`/`fm[key]` at all.
|
|
189
|
+
* (b) the tiers in the opposite order — `typeof v === 'boolean' ||
|
|
190
|
+
* typeof v === 'number'` — never matched, because the pattern
|
|
191
|
+
* hardcoded 'number' before 'boolean' with no alternative ordering.
|
|
192
|
+
* This one is plausible from an ordinary code-review reformat, not
|
|
193
|
+
* deliberate evasion.
|
|
194
|
+
* (c) the ladder split across two lines, or written as two separate `if`
|
|
195
|
+
* statements instead of one `||` chain — never matched, because
|
|
196
|
+
* detection ran per-line with no tolerance for the pair spanning more
|
|
197
|
+
* than one line.
|
|
198
|
+
* The fix: OPERAND_SOURCE (identifier / dotted member / computed access,
|
|
199
|
+
* compared as captured TEXT rather than a bare-identifier backreference),
|
|
200
|
+
* TYPEOF_TIER_CLAUSE_RE (one clause at a time, tier-order-independent,
|
|
201
|
+
* collected via a `Map<operand, Set<tier>>`), and a small bounded
|
|
202
|
+
* LADDER_WINDOW_LINES sliding window so the two required clauses need only
|
|
203
|
+
* sit within a few lines of each other, not on the identical line. All
|
|
204
|
+
* three evasion shapes above are now covered by
|
|
205
|
+
* `tests/state-field-drift.test.cjs` (D3c/D3d/D3e).
|
|
206
|
+
*
|
|
207
|
+
* KNOWN, ACCEPTED limits of this scan (honest, not exhaustive by
|
|
208
|
+
* construction — this is a bounded regex-based scan, not a parser):
|
|
209
|
+
* - Cross-function: a re-derivation whose ladder and fallback call sit in
|
|
210
|
+
* two DIFFERENT named functions with no shared enclosing scope (e.g. a
|
|
211
|
+
* ladder in one file-level helper, consumed by a caller in another
|
|
212
|
+
* function that itself calls `stateExtractField(` for an unrelated
|
|
213
|
+
* field) is not caught — this guard's unit is "one named function's own
|
|
214
|
+
* body, including its nested closures", not the whole call graph. That
|
|
215
|
+
* is left to code review, not this regex. This co-occurrence check is
|
|
216
|
+
* deliberately FUNCTION-SCOPED with NO line-distance window at all (see
|
|
217
|
+
* the module-level "DETECTION" note above) — do not confuse this with
|
|
218
|
+
* LADDER_WINDOW_LINES, which bounds a different, much tighter pairing
|
|
219
|
+
* (the ladder's own two clauses, which are always part of the SAME
|
|
220
|
+
* conditional expression by construction, not an arbitrary call
|
|
221
|
+
* anywhere later in the function).
|
|
222
|
+
* - Ladder clauses further apart than LADDER_WINDOW_LINES lines: a ladder
|
|
223
|
+
* whose 'number' and 'boolean' clauses are more than
|
|
224
|
+
* LADDER_WINDOW_LINES lines apart (e.g. separated by an unrelated
|
|
225
|
+
* intervening block of code, not just the couple of lines a single
|
|
226
|
+
* conditional or two adjacent `if`s span) is not caught. No such shape
|
|
227
|
+
* has been observed in this codebase; if one appears, raise
|
|
228
|
+
* LADDER_WINDOW_LINES rather than silently accepting the miss.
|
|
229
|
+
* - Non-`typeof`-shaped coercion checks: a ladder rewritten through a
|
|
230
|
+
* `switch (typeof v)`, a helper function abstracting the check (e.g.
|
|
231
|
+
* `isNumberOrBoolean(v)`), or any comparison operator other than
|
|
232
|
+
* `===` (e.g. `typeof v == 'number'`) is not recognised — the pattern
|
|
233
|
+
* is `typeof OPERAND === 'TIER'` literally, not "any type-coercion
|
|
234
|
+
* test with equivalent runtime behaviour."
|
|
235
|
+
* - Nested computed access: `a[b[c]]` is not modelled as a single
|
|
236
|
+
* operand — the computed-content class excludes `]`, so nested
|
|
237
|
+
* brackets truncate the captured operand at the first `]` rather than
|
|
238
|
+
* matching the whole expression. Not observed in this codebase's
|
|
239
|
+
* ladders today.
|
|
240
|
+
* - Whitespace-sensitive operand identity: `fm[key]` and `fm[ key ]` are
|
|
241
|
+
* compared as distinct operand TEXT (no normalisation), so a ladder
|
|
242
|
+
* whose two clauses format the same computed access differently could
|
|
243
|
+
* under-detect. Not observed in this codebase's ladders today.
|
|
244
|
+
*/
|
|
245
|
+
|
|
246
|
+
const path = require('node:path');
|
|
247
|
+
const driftScan = require('./lib/drift-scan.cjs');
|
|
248
|
+
const { MAX_REGEX_LITERAL_LEN, sanitizeForReport, scanTree } = driftScan;
|
|
249
|
+
|
|
250
|
+
// A ladder operand: a bare identifier (`v`), a dotted member expression
|
|
251
|
+
// (`fm.key`, `fm.a.b`), or a computed/bracket access (`fm[key]`), repeated
|
|
252
|
+
// via a single bounded alternation. No nested/overlapping quantifiers: the
|
|
253
|
+
// bracket-content class (`[^\]\r\n]{1,80}`) is one bounded character class,
|
|
254
|
+
// not a quantifier nested inside another quantifier, so there is nothing
|
|
255
|
+
// for a backtracking engine to explore. The captured TEXT (not a bare-
|
|
256
|
+
// identifier backreference) is what two clauses are compared against for
|
|
257
|
+
// "same operand" — see TYPEOF_TIER_CLAUSE_RE below and buildFunctionInfo's
|
|
258
|
+
// ladder-window accumulation.
|
|
259
|
+
const OPERAND_SOURCE = String.raw`[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[^\]\r\n]{1,80}\])*`;
|
|
260
|
+
|
|
261
|
+
// One ladder clause: `typeof OPERAND === 'TIER'`, TIER being one of the
|
|
262
|
+
// three coercion tiers this derivation ever compares against. Run with the
|
|
263
|
+
// `g` flag over a single line's `detect` text so every clause that line
|
|
264
|
+
// carries is collected (a single-line `||` chain carries two or three; a
|
|
265
|
+
// clause split onto its own line, or written as a standalone `if`, carries
|
|
266
|
+
// one). No trailing `\b` after the closing quote: the quote itself is
|
|
267
|
+
// already an unambiguous, non-word boundary — see the historical note this
|
|
268
|
+
// replaced for why a trailing `\b` there silently matches nothing at all.
|
|
269
|
+
const TYPEOF_TIER_CLAUSE_RE = new RegExp(String.raw`\btypeof\s+(${OPERAND_SOURCE})\s*===\s*'(number|boolean|string)'`, 'g');
|
|
270
|
+
|
|
271
|
+
// A ladder is CONFIRMED when the SAME operand carries both a 'number' clause
|
|
272
|
+
// and a 'boolean' clause (in either order; an additional 'string' clause
|
|
273
|
+
// anywhere among them does not prevent this) within this many consecutive
|
|
274
|
+
// lines of each other. Small and bounded, and DELIBERATELY NOT the same
|
|
275
|
+
// concept as the ladder-to-`stateExtractField(` co-occurrence check, which
|
|
276
|
+
// stays function-scoped with no window at all (see the module header's
|
|
277
|
+
// "DETECTION" and "LADDER DETECTION DETAIL" notes for why these two
|
|
278
|
+
// pairings are not interchangeable).
|
|
279
|
+
const LADDER_WINDOW_LINES = 6;
|
|
280
|
+
|
|
281
|
+
// The body-fallback call whose presence, inside a ladder-bearing function,
|
|
282
|
+
// makes that function a re-derivation of the "frontmatter-else-body" grammar.
|
|
283
|
+
const STATE_EXTRACT_FIELD_CALL_RE = /\bstateExtractField\(/;
|
|
284
|
+
|
|
285
|
+
// Named function scope openers. Each requires its own opening `{` on the
|
|
286
|
+
// SAME line as the signature — see the header's "FUNCTION ATTRIBUTION"
|
|
287
|
+
// paragraph for the documented limitation and why it does not affect either
|
|
288
|
+
// real copy this guard was written against.
|
|
289
|
+
// - `function NAME(...) {` — top-level OR nested, any indentation.
|
|
290
|
+
const FUNCTION_DECL_RE = /\bfunction\s+([A-Za-z_$][\w$]*)\s*\(/;
|
|
291
|
+
// - `const NAME = (...): ReturnType => {` — block-bodied arrow assigned to
|
|
292
|
+
// a const. `[^)]*` and `[^=]*` are bounded, single-purpose character
|
|
293
|
+
// classes (no nested/overlapping quantifiers): the former stops at the
|
|
294
|
+
// parameter list's closing paren, the latter at the arrow itself.
|
|
295
|
+
const ARROW_CONST_RE = /\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*\([^)]*\)\s*(?::\s*[^=]+)?=>\s*\{/;
|
|
296
|
+
|
|
297
|
+
// ADR-3180 Decision 4(d): a guard's scan surface is every AUTHORED surface
|
|
298
|
+
// that can EXPRESS the derivation, not `src/` — reading "whole-repo scan" as
|
|
299
|
+
// "the whole `src/` tree" is itself the forbidden allowlist, one directory
|
|
300
|
+
// wide. This derivation is expressed in TWO languages: TypeScript under
|
|
301
|
+
// `src/` (the ladder + `stateExtractField(` shape PASS 1/2 above detect), and
|
|
302
|
+
// PROSE in the workflow/command/agent/skill markdown that ships to every
|
|
303
|
+
// runtime — `gsd-core/workflows/smart-entry.md`'s "Extract: `status`
|
|
304
|
+
// (frontmatter `status:` or body `**Status:**`)" is exactly this chain,
|
|
305
|
+
// hand-described rather than called. `SCAN_DIRS` therefore covers both;
|
|
306
|
+
// `findPromptFieldDrift` (below `findStateFieldDrift`) is the markdown-side
|
|
307
|
+
// detector, dispatched by extension in `scanRepo`'s `onFile`. Mirrors
|
|
308
|
+
// `lint-planning-prompt-drift.cjs`'s own prompt-layer surface exactly
|
|
309
|
+
// (`gsd-core/workflows`, `commands`, `agents`, `skills`).
|
|
310
|
+
const SCAN_DIRS = ['src', 'gsd-core/workflows', 'commands', 'agents', 'skills'];
|
|
311
|
+
const SCAN_EXT = new Set(['.cts', '.ts', '.mts', '.md']);
|
|
312
|
+
|
|
313
|
+
// The designated owner (issue #3187 Phase 5, ADR-3180 §7.7).
|
|
314
|
+
const OWNER_FILE = path.join('src', 'state-document.cts');
|
|
315
|
+
|
|
316
|
+
// Per ADR-3180 Decision 4(a): function-scoped, NEVER a bare file allowlist —
|
|
317
|
+
// Decision 4(d) records that a whole-file exemption on the owner is
|
|
318
|
+
// precisely how `getMilestoneInfo` stayed invisible to an earlier guard.
|
|
319
|
+
// Only `stateFieldValue` itself is exempt: it IS the canonical
|
|
320
|
+
// frontmatter-scalar-then-body-field chain (holds the ladder AND calls
|
|
321
|
+
// `stateExtractField(` by construction, in its own body — that is its whole
|
|
322
|
+
// job), not a copy of it. Every OTHER function in this same file — including
|
|
323
|
+
// any future re-derivation added anywhere else in state-document.cts — is
|
|
324
|
+
// still scanned and still flagged; nothing else in this file is exempt.
|
|
325
|
+
const FUNCTION_SCOPED_EXEMPTIONS = new Map([[OWNER_FILE, new Set(['stateFieldValue'])]]);
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Tokenize the WHOLE file into TWO parallel per-line views, from ONE
|
|
329
|
+
* single-pass, escape-aware character scan (not a regex — no backtracking
|
|
330
|
+
* cost to bound):
|
|
331
|
+
* - `detect[i]`: comments stripped, but string/template literal contents
|
|
332
|
+
* KEPT VERBATIM (quotes included). This is what the detection regexes
|
|
333
|
+
* (LADDER_RE / STATE_EXTRACT_FIELD_CALL_RE / FUNCTION_DECL_RE /
|
|
334
|
+
* ARROW_CONST_RE) run against — they need the literal quoted text
|
|
335
|
+
* `'number'` / `'boolean'` to still be present.
|
|
336
|
+
* - `braces[i]`: comments AND string/template literal CONTENTS stripped,
|
|
337
|
+
* used only for brace-depth counting, so a brace character written
|
|
338
|
+
* inside a string (e.g. `{ label: '{' }`) never perturbs the count.
|
|
339
|
+
*
|
|
340
|
+
* This REPLACES two earlier, narrower designs in turn:
|
|
341
|
+
* 1. Two independent per-line helpers (`stripComments` + a separate
|
|
342
|
+
* `stripStringLiterals`), each with no cross-line state. That design
|
|
343
|
+
* missed a `/* ... *\/`-style block comment whose CLOSING line also
|
|
344
|
+
* carries trailing real code (e.g. a `catch { /* comment` opener
|
|
345
|
+
* followed by ` * more comment. *\/ }` on a later line): the old
|
|
346
|
+
* per-line `stripComments` heuristic ("a line whose trimmed text starts
|
|
347
|
+
* with `*` is entirely comment") blanked that closing line WHOLESALE,
|
|
348
|
+
* silently dropping the real `}` it also carried, which left a
|
|
349
|
+
* function's brace-depth frame permanently open and made every LATER
|
|
350
|
+
* `stateExtractField(` call in the file — inside unrelated functions —
|
|
351
|
+
* inherit that stale frame's ladder-bearing status (`src/state.cts`'s
|
|
352
|
+
* `cmdStateValidate`, which has no ladder of its own, was falsely
|
|
353
|
+
* flagged this way).
|
|
354
|
+
* 2. A single merged output that dropped string CONTENTS unconditionally.
|
|
355
|
+
* That fixed (1) but broke detection outright: LADDER_RE needs the
|
|
356
|
+
* literal text `'number'`/`'boolean'` (with quotes) to match, and a
|
|
357
|
+
* merged output that strips quote contents for brace-safety also
|
|
358
|
+
* erases the very tokens the ladder regex looks for — every ladder
|
|
359
|
+
* line silently stopped matching. Two views, not one, is what lets
|
|
360
|
+
* each consumer see what it actually needs.
|
|
361
|
+
*
|
|
362
|
+
* `inBlockComment` and `inTemplate` are threaded ACROSS lines so a block
|
|
363
|
+
* comment or a multi-line backtick template literal spanning several source
|
|
364
|
+
* lines is tracked correctly regardless of what trails its closing
|
|
365
|
+
* delimiter. Regex literals are NOT specially recognised (documented,
|
|
366
|
+
* narrow, known limitation, same as the sibling drift guards'
|
|
367
|
+
* `readRegexLiteralAt`-free scans): a `/` is only ever treated as a comment
|
|
368
|
+
* opener when immediately followed by another `/` or `*`, so an ordinary
|
|
369
|
+
* regex literal's slashes pass through as plain characters and any brace
|
|
370
|
+
* inside one (e.g. `/x{2,3}/`) is counted like any other character —
|
|
371
|
+
* verified harmless for every regex literal actually present in `src/`
|
|
372
|
+
* today because each is balanced (equal opens/closes) on its own line, so it
|
|
373
|
+
* never desyncs the running depth total even though the individual
|
|
374
|
+
* characters are not "understood" as a literal.
|
|
375
|
+
*/
|
|
376
|
+
function scanCode(lines) {
|
|
377
|
+
const detect = new Array(lines.length);
|
|
378
|
+
const braces = new Array(lines.length);
|
|
379
|
+
let inBlockComment = false;
|
|
380
|
+
let inTemplate = false;
|
|
381
|
+
for (let li = 0; li < lines.length; li++) {
|
|
382
|
+
const line = lines[li];
|
|
383
|
+
let outDetect = '';
|
|
384
|
+
let outBraces = '';
|
|
385
|
+
let i = 0;
|
|
386
|
+
if (inTemplate) {
|
|
387
|
+
const start = i;
|
|
388
|
+
while (i < line.length) {
|
|
389
|
+
if (line[i] === '\\') {
|
|
390
|
+
i += 2;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (line[i] === '`') {
|
|
394
|
+
i++;
|
|
395
|
+
inTemplate = false;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
i++;
|
|
399
|
+
}
|
|
400
|
+
outDetect += line.slice(start, i); // template contents kept verbatim for detect
|
|
401
|
+
if (inTemplate) {
|
|
402
|
+
detect[li] = outDetect;
|
|
403
|
+
braces[li] = ''; // whole line still inside the unterminated template
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
while (i < line.length) {
|
|
408
|
+
if (inBlockComment) {
|
|
409
|
+
const close = line.indexOf('*/', i);
|
|
410
|
+
if (close === -1) {
|
|
411
|
+
i = line.length;
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
i = close + 2;
|
|
415
|
+
inBlockComment = false;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const ch = line[i];
|
|
419
|
+
if (ch === '/' && line[i + 1] === '/') {
|
|
420
|
+
i = line.length; // rest of line is a line comment
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
if (ch === '/' && line[i + 1] === '*') {
|
|
424
|
+
inBlockComment = true;
|
|
425
|
+
i += 2;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (ch === "'" || ch === '"') {
|
|
429
|
+
const quote = ch;
|
|
430
|
+
const start = i;
|
|
431
|
+
let j = i + 1;
|
|
432
|
+
while (j < line.length) {
|
|
433
|
+
if (line[j] === '\\') {
|
|
434
|
+
j += 2; // escape consumes the next character, whatever it is
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (line[j] === quote) {
|
|
438
|
+
j++;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
j++;
|
|
442
|
+
}
|
|
443
|
+
outDetect += line.slice(start, j); // string kept verbatim for detect
|
|
444
|
+
// (nothing appended to outBraces — string contents excluded from depth counting)
|
|
445
|
+
i = j;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (ch === '`') {
|
|
449
|
+
const start = i;
|
|
450
|
+
let j = i + 1;
|
|
451
|
+
let closed = false;
|
|
452
|
+
while (j < line.length) {
|
|
453
|
+
if (line[j] === '\\') {
|
|
454
|
+
j += 2;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (line[j] === '`') {
|
|
458
|
+
j++;
|
|
459
|
+
closed = true;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
j++;
|
|
463
|
+
}
|
|
464
|
+
if (!closed) {
|
|
465
|
+
outDetect += line.slice(start); // rest of line kept verbatim for detect
|
|
466
|
+
inTemplate = true;
|
|
467
|
+
i = line.length;
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
outDetect += line.slice(start, j); // template kept verbatim for detect
|
|
471
|
+
i = j;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
outDetect += ch;
|
|
475
|
+
outBraces += ch;
|
|
476
|
+
i++;
|
|
477
|
+
}
|
|
478
|
+
detect[li] = outDetect;
|
|
479
|
+
braces[li] = outBraces;
|
|
480
|
+
}
|
|
481
|
+
return { detect, braces };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Pure: find every unsanctioned STATE.md field-extraction fallback-chain
|
|
486
|
+
* re-derivation in `text`. `relPath` is the repo-relative path, used both to
|
|
487
|
+
* report file:line and to apply the function-scoped owner exemptions above.
|
|
488
|
+
*
|
|
489
|
+
* Two passes over the same line array:
|
|
490
|
+
* PASS 1 walks the file once, maintaining a brace-depth stack of open
|
|
491
|
+
* named function frames, and records which function NAMES are
|
|
492
|
+
* ladder-bearing (transitively — see the header's "TRANSITIVE ladder
|
|
493
|
+
* attribution" paragraph) and, per source line, which frame is
|
|
494
|
+
* INNERMOST at that line (for call attribution) — {@link buildFunctionInfo}.
|
|
495
|
+
* PASS 2 walks the lines again; any `stateExtractField(` call whose
|
|
496
|
+
* innermost enclosing function is ladder-bearing (and not
|
|
497
|
+
* function-scoped-exempt) is a violation.
|
|
498
|
+
* Returns [{ line, found }].
|
|
499
|
+
*/
|
|
500
|
+
function buildFunctionInfo(lines) {
|
|
501
|
+
const { detect, braces } = scanCode(lines);
|
|
502
|
+
const ladderBearing = new Set();
|
|
503
|
+
const innermostAt = new Array(lines.length).fill(null);
|
|
504
|
+
const stack = []; // { name, openDepth }
|
|
505
|
+
let depth = 0;
|
|
506
|
+
// A `function NAME(` match whose own line carries no `{` (a multi-line
|
|
507
|
+
// signature — parameters and/or a return-type annotation spilling onto
|
|
508
|
+
// later lines before the body opens) — awaiting the line that actually
|
|
509
|
+
// carries its opening brace. See the header's "FUNCTION ATTRIBUTION"
|
|
510
|
+
// paragraph and this function's own doc comment for why this cannot be
|
|
511
|
+
// pushed onto `stack` immediately: pushing it with `openDepth` recorded
|
|
512
|
+
// BEFORE its own `{` is counted produces a frame whose `openDepth` is the
|
|
513
|
+
// ENCLOSING scope's depth, not its own — for a top-level function that is
|
|
514
|
+
// `openDepth: 0`, and since real code never reaches negative depth, a
|
|
515
|
+
// frame pushed with `openDepth: 0` can NEVER pop (`depth < 0` never
|
|
516
|
+
// fires): it would sit at the bottom of `stack` for the rest of the file,
|
|
517
|
+
// and every subsequent line with no OTHER open frame would be
|
|
518
|
+
// misattributed to it. `ARROW_CONST_RE` never needs this deferral — its
|
|
519
|
+
// pattern requires `=>\s*\{` literally, so it only ever matches on a line
|
|
520
|
+
// that already carries the brace.
|
|
521
|
+
let pendingDeclName = null;
|
|
522
|
+
// LADDER WINDOW: a rolling buffer of every `typeof OPERAND === 'TIER'`
|
|
523
|
+
// clause seen in the last LADDER_WINDOW_LINES lines, oldest-first. On
|
|
524
|
+
// each line, new clauses from that line are appended, then entries older
|
|
525
|
+
// than the window are pruned from the front. A ladder is confirmed the
|
|
526
|
+
// moment the SAME operand has accumulated both a 'number' and a
|
|
527
|
+
// 'boolean' entry inside the buffer — order-independent, and regardless
|
|
528
|
+
// of whether the two clauses came from one `||` chain split across
|
|
529
|
+
// lines, one single-line chain, or two entirely separate `if`
|
|
530
|
+
// statements. See LADDER_WINDOW_LINES's own comment for why this is
|
|
531
|
+
// bounded small and is NOT the same concept as the (unbounded,
|
|
532
|
+
// function-scoped) ladder-to-`stateExtractField(` co-occurrence below.
|
|
533
|
+
const recentClauses = []; // { line, operand, tier }
|
|
534
|
+
for (let i = 0; i < lines.length; i++) {
|
|
535
|
+
const detectCode = detect[i];
|
|
536
|
+
const braceCode = braces[i];
|
|
537
|
+
|
|
538
|
+
let immediateName = null;
|
|
539
|
+
if (detectCode.trim()) {
|
|
540
|
+
const arrowMatch = ARROW_CONST_RE.exec(detectCode);
|
|
541
|
+
if (arrowMatch) {
|
|
542
|
+
immediateName = arrowMatch[1];
|
|
543
|
+
} else {
|
|
544
|
+
const declMatch = FUNCTION_DECL_RE.exec(detectCode);
|
|
545
|
+
// A NEW `function NAME(` match replaces any still-pending name
|
|
546
|
+
// rather than stacking deferrals — this scanner tracks at most one
|
|
547
|
+
// pending declaration at a time (two `function` keywords on
|
|
548
|
+
// consecutive lines with neither closing its signature first is not
|
|
549
|
+
// a shape that occurs in this codebase's style).
|
|
550
|
+
if (declMatch) pendingDeclName = declMatch[1];
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const opens = (braceCode.match(/\{/g) || []).length;
|
|
555
|
+
const closes = (braceCode.match(/\}/g) || []).length;
|
|
556
|
+
depth += opens - closes;
|
|
557
|
+
|
|
558
|
+
if (immediateName) stack.push({ name: immediateName, openDepth: depth });
|
|
559
|
+
|
|
560
|
+
if (pendingDeclName) {
|
|
561
|
+
if (opens > 0) {
|
|
562
|
+
// First line whose brace count actually increases — its `{` is
|
|
563
|
+
// counted in THIS line's `opens`, so `depth` here correctly
|
|
564
|
+
// reflects "inside the function", matching every other frame's
|
|
565
|
+
// convention (push AFTER updating depth for the pushing line).
|
|
566
|
+
stack.push({ name: pendingDeclName, openDepth: depth });
|
|
567
|
+
pendingDeclName = null;
|
|
568
|
+
} else if (detectCode.includes(';')) {
|
|
569
|
+
// The statement terminated before any `{` appeared — a type-only
|
|
570
|
+
// declaration, an ambient `declare function`, or an overload
|
|
571
|
+
// signature, none of which open a function body. Abandon the
|
|
572
|
+
// pending name rather than stranding it to match a `{` that
|
|
573
|
+
// belongs to unrelated later code. (A `;` inside a string literal
|
|
574
|
+
// on this line would also trigger this — accepted, narrow, known
|
|
575
|
+
// limitation: a default-parameter string containing `;` on the
|
|
576
|
+
// SAME line as an unterminated multi-line signature has not been
|
|
577
|
+
// observed in this codebase.)
|
|
578
|
+
pendingDeclName = null;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
while (stack.length > 0 && depth < stack[stack.length - 1].openDepth) stack.pop();
|
|
583
|
+
|
|
584
|
+
innermostAt[i] = stack.length > 0 ? stack[stack.length - 1].name : null;
|
|
585
|
+
|
|
586
|
+
if (detectCode.trim()) {
|
|
587
|
+
TYPEOF_TIER_CLAUSE_RE.lastIndex = 0;
|
|
588
|
+
let clauseMatch;
|
|
589
|
+
while ((clauseMatch = TYPEOF_TIER_CLAUSE_RE.exec(detectCode)) !== null) {
|
|
590
|
+
recentClauses.push({ line: i, operand: clauseMatch[1], tier: clauseMatch[2] });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// Prune clauses that have fallen outside the small bounded ladder window.
|
|
594
|
+
while (recentClauses.length > 0 && recentClauses[0].line <= i - LADDER_WINDOW_LINES) {
|
|
595
|
+
recentClauses.shift();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
let ladderConfirmed = false;
|
|
599
|
+
if (recentClauses.length > 0) {
|
|
600
|
+
const tiersByOperand = new Map();
|
|
601
|
+
for (const clause of recentClauses) {
|
|
602
|
+
let tiers = tiersByOperand.get(clause.operand);
|
|
603
|
+
if (!tiers) {
|
|
604
|
+
tiers = new Set();
|
|
605
|
+
tiersByOperand.set(clause.operand, tiers);
|
|
606
|
+
}
|
|
607
|
+
tiers.add(clause.tier);
|
|
608
|
+
}
|
|
609
|
+
for (const tiers of tiersByOperand.values()) {
|
|
610
|
+
if (tiers.has('number') && tiers.has('boolean')) {
|
|
611
|
+
ladderConfirmed = true;
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (ladderConfirmed) {
|
|
618
|
+
// Transitive: every frame currently open (not just the innermost) is
|
|
619
|
+
// ladder-bearing, because a nested closure's body is lexically part of
|
|
620
|
+
// every one of its enclosing functions' own bodies.
|
|
621
|
+
for (const frame of stack) ladderBearing.add(frame.name);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return { ladderBearing, innermostAt, detect };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ─── PROMPT-LAYER PROSE DETECTION (ADR-3180 Decision 4(d)) ─────────────────
|
|
628
|
+
//
|
|
629
|
+
// The SAME #1760 fallback chain — "prefer the frontmatter scalar, else fall
|
|
630
|
+
// back to the body field" — expressed as MARKDOWN PROSE describing the
|
|
631
|
+
// derivation to an agent, rather than TypeScript re-deriving it. Detection is
|
|
632
|
+
// intentionally narrow, mirroring `lint-planning-prompt-drift.cjs`'s own
|
|
633
|
+
// precedent: a markdown line is a prose re-derivation only when it carries
|
|
634
|
+
// ALL FOUR, on the SAME line:
|
|
635
|
+
// (a) a backtick-quoted YAML-style frontmatter key token — a lowercase
|
|
636
|
+
// identifier immediately followed by a colon, inside backticks, e.g.
|
|
637
|
+
// `` `status:` ``. `FRONTMATTER_KEY_TOKEN_RE`.
|
|
638
|
+
// (b) a backtick-quoted Markdown BOLD body-field token, e.g.
|
|
639
|
+
// `` `**Status:**` ``. `BODY_BOLD_TOKEN_RE`.
|
|
640
|
+
// (c) the word "frontmatter" (case-insensitive, word-bounded) anywhere on
|
|
641
|
+
// the line.
|
|
642
|
+
// (d) the word "body" (case-insensitive, word-bounded) anywhere on the
|
|
643
|
+
// line.
|
|
644
|
+
// (c) and (d) are what distinguish a line DESCRIBING an fm-then-body
|
|
645
|
+
// PRECEDENCE from a line that merely happens to carry two backtick-quoted
|
|
646
|
+
// tokens shaped like (a) and (b) for unrelated reasons — both words are
|
|
647
|
+
// required so an incidental co-occurrence (e.g. a table row naming an
|
|
648
|
+
// unrelated frontmatter key and, several columns over, an unrelated bold
|
|
649
|
+
// body label) cannot false-positive on tokens alone.
|
|
650
|
+
// All four regexes are small, bounded, single fixed character classes with
|
|
651
|
+
// no nested/overlapping quantifiers — nothing for a backtracking engine to
|
|
652
|
+
// explore (`npm run lint:ci` runs CodeQL js/redos over this repo, the same
|
|
653
|
+
// discipline `findStateFieldDrift`'s own regexes document above).
|
|
654
|
+
const FRONTMATTER_KEY_TOKEN_RE = /`[a-z][a-z0-9_]*:`/;
|
|
655
|
+
const BODY_BOLD_TOKEN_RE = /`\*\*[^*`]{1,80}\*\*`/;
|
|
656
|
+
const FRONTMATTER_WORD_RE = /\bfrontmatter\b/i;
|
|
657
|
+
const BODY_WORD_RE = /\bbody\b/i;
|
|
658
|
+
|
|
659
|
+
// Per ADR-3180 Decision 4(d)/(e): `gsd-core/workflows/smart-entry.md`'s
|
|
660
|
+
// Fallback step 1 ("`gsd-tools` itself is broken") is a PERMANENT, by-
|
|
661
|
+
// construction exemption — NOT debt with an owner, and therefore NOT
|
|
662
|
+
// modelled as `lint-planning-prompt-drift.cjs`'s shrink-only ratchet
|
|
663
|
+
// baseline (which exists specifically to acknowledge sites with a removal
|
|
664
|
+
// issue, per Decision 4(e)). This site can never be migrated onto
|
|
665
|
+
// `src/state-document.cjs`'s `stateFieldValue`: the surrounding step exists
|
|
666
|
+
// PRECISELY for when the CLI itself cannot run (`Cannot find module ...` /
|
|
667
|
+
// Node crash — probed one line above this one), so by construction it cannot
|
|
668
|
+
// call the canonical owner it is standing in for. Fabricating a "removal
|
|
669
|
+
// issue" for something that can never be removed would misrepresent it as
|
|
670
|
+
// ratchetable debt it is not.
|
|
671
|
+
//
|
|
672
|
+
// Keyed on (file, TRIMMED source text) — never a line number, which churns
|
|
673
|
+
// on any unrelated edit to the same file — mirroring the ratchet's own key
|
|
674
|
+
// shape (`lint-planning-prompt-drift.cjs`) even though this exemption is not
|
|
675
|
+
// itself a ratchet. `file` uses the SAME native-separator relPath shape as
|
|
676
|
+
// `OWNER_FILE`/`FUNCTION_SCOPED_EXEMPTIONS` above (this guard does not
|
|
677
|
+
// POSIX-normalize elsewhere, so this exemption does not either, for
|
|
678
|
+
// consistency within the one file).
|
|
679
|
+
const PROMPT_LAYER_EXEMPTIONS = new Map([
|
|
680
|
+
[
|
|
681
|
+
path.join('gsd-core', 'workflows', 'smart-entry.md'),
|
|
682
|
+
new Map([
|
|
683
|
+
[
|
|
684
|
+
"- Read `.planning/STATE.md` (frontmatter + body) with the Read tool. Extract: `status` (frontmatter `status:` or body `**Status:**`), `Phase:` from the body, `total_phases`/`percent` from a nested `progress:` frontmatter object if present, and any `## Blockers` items.",
|
|
685
|
+
'gsd-tools-down fallback (smart-entry.md Fallback step 1): runs only when gsd-tools itself cannot run, so it cannot call the canonical owner it substitutes for — permanent by construction, not removable debt.',
|
|
686
|
+
],
|
|
687
|
+
]),
|
|
688
|
+
],
|
|
689
|
+
]);
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Pure: find every prose re-derivation of the STATE.md field-extraction
|
|
693
|
+
* fallback chain in a markdown file's `text`. Returns `[{ line, found }]` for
|
|
694
|
+
* every UNEXEMPTED match — `relPath` (native-separator, matching
|
|
695
|
+
* `PROMPT_LAYER_EXEMPTIONS`'s keys) is consulted only for the exemption
|
|
696
|
+
* lookup, exactly as `findStateFieldDrift` consults it for
|
|
697
|
+
* `FUNCTION_SCOPED_EXEMPTIONS`.
|
|
698
|
+
*/
|
|
699
|
+
function findPromptFieldDrift(text, relPath) {
|
|
700
|
+
const out = [];
|
|
701
|
+
const lines = text.split('\n');
|
|
702
|
+
const exemptTexts = PROMPT_LAYER_EXEMPTIONS.get(relPath) || null;
|
|
703
|
+
for (let i = 0; i < lines.length; i++) {
|
|
704
|
+
const line = lines[i];
|
|
705
|
+
if (!FRONTMATTER_KEY_TOKEN_RE.test(line)) continue;
|
|
706
|
+
if (!BODY_BOLD_TOKEN_RE.test(line)) continue;
|
|
707
|
+
if (!FRONTMATTER_WORD_RE.test(line)) continue;
|
|
708
|
+
if (!BODY_WORD_RE.test(line)) continue;
|
|
709
|
+
const trimmed = line.trim();
|
|
710
|
+
if (exemptTexts && exemptTexts.has(trimmed)) continue;
|
|
711
|
+
out.push({ line: i + 1, found: trimmed.slice(0, MAX_REGEX_LITERAL_LEN) });
|
|
712
|
+
}
|
|
713
|
+
return out;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function findStateFieldDrift(text, relPath) {
|
|
717
|
+
const out = [];
|
|
718
|
+
const lines = text.split('\n');
|
|
719
|
+
const exemptFunctions = FUNCTION_SCOPED_EXEMPTIONS.get(relPath) || null;
|
|
720
|
+
const { ladderBearing, innermostAt, detect } = buildFunctionInfo(lines);
|
|
721
|
+
|
|
722
|
+
for (let i = 0; i < lines.length; i++) {
|
|
723
|
+
const detectCode = detect[i];
|
|
724
|
+
if (!detectCode.trim()) continue;
|
|
725
|
+
if (!STATE_EXTRACT_FIELD_CALL_RE.test(detectCode)) continue;
|
|
726
|
+
|
|
727
|
+
const fn = innermostAt[i];
|
|
728
|
+
if (!fn || !ladderBearing.has(fn)) continue;
|
|
729
|
+
if (exemptFunctions && exemptFunctions.has(fn)) continue;
|
|
730
|
+
|
|
731
|
+
out.push({ line: i + 1, found: lines[i].trim().slice(0, MAX_REGEX_LITERAL_LEN) });
|
|
732
|
+
}
|
|
733
|
+
return out;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Scan the authored source tree (TypeScript under `src/`, prose in the
|
|
738
|
+
* prompt-layer markdown — ADR-3180 Decision 4(d)) and return every
|
|
739
|
+
* unsanctioned re-derivation, each annotated with the repo-relative file
|
|
740
|
+
* path. Dispatches by extension: `.md` files run the prose detector
|
|
741
|
+
* (`findPromptFieldDrift`), everything else (`.cts`/`.ts`/`.mts`) runs the
|
|
742
|
+
* code detector (`findStateFieldDrift`) — the two derivations are expressed
|
|
743
|
+
* in different languages and need different detection shapes over the same
|
|
744
|
+
* scan surface.
|
|
745
|
+
*/
|
|
746
|
+
function scanRepo(root) {
|
|
747
|
+
return scanTree({
|
|
748
|
+
root,
|
|
749
|
+
scanDirs: SCAN_DIRS,
|
|
750
|
+
scanExt: SCAN_EXT,
|
|
751
|
+
onFile(rel, text) {
|
|
752
|
+
// `rel` is already the REAL (canonical) path (scanTree resolves
|
|
753
|
+
// symlinks before calling onFile), so this — and
|
|
754
|
+
// FUNCTION_SCOPED_EXEMPTIONS/PROMPT_LAYER_EXEMPTIONS above, also keyed
|
|
755
|
+
// on `rel` — match consistently regardless of which symlink reached
|
|
756
|
+
// the file.
|
|
757
|
+
const finder = path.extname(rel) === '.md' ? findPromptFieldDrift : findStateFieldDrift;
|
|
758
|
+
return finder(text, rel).map((d) => ({ file: rel, ...d }));
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function main() {
|
|
764
|
+
const root = path.join(__dirname, '..');
|
|
765
|
+
const violations = scanRepo(root);
|
|
766
|
+
if (violations.length === 0) {
|
|
767
|
+
process.stdout.write('ok state-field-drift: no unsanctioned STATE.md field-extraction fallback-chain re-derivations found\n');
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
process.stderr.write('state-field-drift: independent re-derivation(s) of the STATE.md frontmatter-else-body field\n');
|
|
771
|
+
process.stderr.write('fallback chain found. Route these call sites through src/state-document.cjs\n');
|
|
772
|
+
process.stderr.write('`stateFieldValue` (issue #3187, ADR-3180 §7.7) instead of re-deriving the ladder:\n');
|
|
773
|
+
for (const d of violations) {
|
|
774
|
+
// `d.file` is exactly as attacker-controlled as `d.found`: a repo can
|
|
775
|
+
// legally track a filename containing control bytes / bidi overrides,
|
|
776
|
+
// and it is a fork-PR-authored value reaching a CI log the same way the
|
|
777
|
+
// matched line text does — sanitize it at the same reporting boundary.
|
|
778
|
+
process.stderr.write(` ${sanitizeForReport(d.file)}:${d.line} ${sanitizeForReport(d.found)}\n`);
|
|
779
|
+
}
|
|
780
|
+
process.exitCode = 1;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (require.main === module) main();
|
|
784
|
+
|
|
785
|
+
module.exports = {
|
|
786
|
+
findStateFieldDrift,
|
|
787
|
+
findPromptFieldDrift,
|
|
788
|
+
buildFunctionInfo,
|
|
789
|
+
scanRepo,
|
|
790
|
+
OPERAND_SOURCE,
|
|
791
|
+
TYPEOF_TIER_CLAUSE_RE,
|
|
792
|
+
LADDER_WINDOW_LINES,
|
|
793
|
+
STATE_EXTRACT_FIELD_CALL_RE,
|
|
794
|
+
FUNCTION_DECL_RE,
|
|
795
|
+
ARROW_CONST_RE,
|
|
796
|
+
OWNER_FILE,
|
|
797
|
+
FUNCTION_SCOPED_EXEMPTIONS,
|
|
798
|
+
FRONTMATTER_KEY_TOKEN_RE,
|
|
799
|
+
BODY_BOLD_TOKEN_RE,
|
|
800
|
+
PROMPT_LAYER_EXEMPTIONS,
|
|
801
|
+
SCAN_DIRS,
|
|
802
|
+
SCAN_EXT,
|
|
803
|
+
scanCode,
|
|
804
|
+
MAX_REGEX_LITERAL_LEN,
|
|
805
|
+
};
|