@opengsd/gsd-core 1.13.0 → 1.15.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/README.ja-JP.md +3 -3
- package/README.ko-KR.md +3 -3
- package/README.pt-BR.md +3 -3
- package/README.zh-CN.md +3 -3
- package/agents/gsd-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +459 -0
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +360 -0
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +2 -2
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-executor.md +13 -8
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-phase-researcher.md +19 -11
- package/agents/gsd-plan-checker.md +8 -7
- package/agents/gsd-planner.md +12 -8
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-auditor.md +155 -17
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/agents/gsd-verifier.md +10 -9
- package/bin/install.js +848 -163
- package/commands/gsd/autonomous.md +2 -2
- package/commands/gsd/capture.md +1 -1
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +8 -3
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +1 -0
- package/commands/gsd/plan-review-convergence.md +6 -6
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/progress.md +1 -1
- package/commands/gsd/quick-batch.md +1 -1
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/review.md +2 -3
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +672 -146
- package/gsd-core/bin/lib/adr-parser.cjs +4 -2
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +119 -34
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +96 -189
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-validator.cjs +14 -2
- package/gsd-core/bin/lib/check-command-router.cjs +213 -49
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
- package/gsd-core/bin/lib/commands.cjs +823 -112
- package/gsd-core/bin/lib/config-loader.cjs +66 -4
- package/gsd-core/bin/lib/config.cjs +186 -45
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +164 -45
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/frontmatter.cjs +13 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +614 -86
- package/gsd-core/bin/lib/install-engine.cjs +29 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
- package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +37 -13
- package/gsd-core/bin/lib/model-resolver.cjs +253 -53
- package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
- package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
- package/gsd-core/bin/lib/phase-id.cjs +268 -27
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase.cjs +393 -88
- package/gsd-core/bin/lib/plan-document.cjs +49 -1
- package/gsd-core/bin/lib/planning-document.cjs +459 -0
- package/gsd-core/bin/lib/planning-inspect.cjs +52 -19
- package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
- package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/quick-batch.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
- package/gsd-core/bin/lib/roadmap.cjs +356 -42
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
- package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
- package/gsd-core/bin/lib/state-document.cjs +130 -28
- package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
- package/gsd-core/bin/lib/state-transition.cjs +181 -30
- package/gsd-core/bin/lib/state.cjs +265 -27
- package/gsd-core/bin/lib/surface.cjs +77 -3
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
- package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
- package/gsd-core/bin/lib/uat.cjs +9 -1
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
- package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
- package/gsd-core/bin/lib/update-context.cjs +30 -24
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification.cjs +315 -30
- package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
- package/gsd-core/bin/lib/verify.cjs +320 -48
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
- package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
- package/gsd-core/references/checkpoints.md +5 -3
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
- package/gsd-core/references/edge-probe.md +195 -21
- package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
- package/gsd-core/references/execute-phase-wave-guard.md +22 -11
- package/gsd-core/references/gsd-run-resolver.md +1 -1
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +13 -4
- package/gsd-core/references/phase-argument-parsing.md +9 -7
- package/gsd-core/references/phase-id-convention.md +28 -0
- package/gsd-core/references/planner-gap-closure.md +2 -0
- package/gsd-core/references/planner-load-graph-context.md +24 -13
- package/gsd-core/references/planner-verify-command-grounding.md +14 -0
- package/gsd-core/references/planning-config.md +14 -2
- package/gsd-core/references/tdd.md +30 -4
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/worktree-path-safety.md +433 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/templates/verification-report.md +1 -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 +1 -1
- package/gsd-core/workflows/add-tests.md +2 -2
- package/gsd-core/workflows/add-todo.md +6 -5
- package/gsd-core/workflows/ai-integration-phase.md +11 -3
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
- package/gsd-core/workflows/autonomous.md +29 -16
- package/gsd-core/workflows/check-todos.md +6 -4
- package/gsd-core/workflows/cleanup.md +5 -3
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
- package/gsd-core/workflows/code-review-fix.md +108 -22
- package/gsd-core/workflows/code-review.md +216 -73
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +41 -264
- package/gsd-core/workflows/debug.md +3 -3
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +2 -2
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +17 -158
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +10 -3
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- 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 +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
- package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
- package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
- package/gsd-core/workflows/execute-phase.md +83 -172
- package/gsd-core/workflows/execute-plan.md +24 -10
- package/gsd-core/workflows/explore.md +3 -3
- package/gsd-core/workflows/extract-learnings.md +2 -1
- package/gsd-core/workflows/fast.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +2 -2
- package/gsd-core/workflows/help/modes/full.compact.md +398 -0
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- package/gsd-core/workflows/help.md +1 -1
- package/gsd-core/workflows/import.md +2 -2
- package/gsd-core/workflows/inbox.md +2 -2
- package/gsd-core/workflows/ingest-docs.md +3 -3
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +2 -2
- package/gsd-core/workflows/map-codebase.md +52 -5
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +56 -14
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
- package/gsd-core/workflows/new-project.md +39 -209
- package/gsd-core/workflows/new-workspace.md +2 -2
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/note.md +1 -1
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
- package/gsd-core/workflows/plan-phase.md +45 -187
- package/gsd-core/workflows/plan-review-convergence.md +21 -5
- package/gsd-core/workflows/plant-seed.md +62 -20
- package/gsd-core/workflows/pr-branch.md +132 -20
- package/gsd-core/workflows/profile-user.md +2 -2
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
- package/gsd-core/workflows/quick-batch.md +1 -1
- package/gsd-core/workflows/quick.md +29 -10
- package/gsd-core/workflows/reapply-patches.md +86 -6
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +2 -2
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +31 -16
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +3 -2
- package/gsd-core/workflows/settings-advanced.md +30 -10
- package/gsd-core/workflows/settings-integrations.md +2 -3
- package/gsd-core/workflows/settings.md +22 -9
- package/gsd-core/workflows/ship.md +3 -2
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +2 -2
- package/gsd-core/workflows/spec-phase.md +15 -5
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/sync-skills.md +5 -5
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +44 -8
- package/gsd-core/workflows/ui-review.md +18 -4
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/undo.md +339 -20
- package/gsd-core/workflows/update.md +14 -12
- package/gsd-core/workflows/validate-phase.md +3 -2
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
- package/gsd-core/workflows/verify-work.md +101 -196
- package/hooks/dist/gsd-agent-isolation-guard.js +66 -16
- package/hooks/dist/gsd-context-monitor.js +88 -15
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-secret-read-guard.js +71 -19
- package/hooks/dist/gsd-statusline.js +81 -20
- package/hooks/dist/gsd-validate-commit.sh +97 -8
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/gsd-agent-isolation-guard.js +66 -16
- package/hooks/gsd-context-monitor.js +88 -15
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-secret-read-guard.js +71 -19
- package/hooks/gsd-statusline.js +81 -20
- package/hooks/gsd-validate-commit.sh +97 -8
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/package.json +11 -6
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/build-hooks.js +15 -6
- package/scripts/check-contract-drift.cjs +131 -12
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +126 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/command-contract-helpers.cjs +3 -0
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +35 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-loop-host-contract.cjs +69 -0
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +224 -0
- package/scripts/lib/ndjson-reporter.cjs +3 -2
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +287 -0
- package/scripts/lib/suite-detection.cjs +32 -0
- package/scripts/lint-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +47 -3
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-id-drift.cjs +623 -13
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-response-language-coverage.cjs +9 -3
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +29 -9
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
- package/scripts/prompt-injection-scan.sh +18 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-autonomous/SKILL.md +2 -2
- package/skills/gsd-capture/SKILL.md +1 -1
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +8 -3
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -0
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-progress/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +1 -1
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review/SKILL.md +2 -3
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
|
@@ -20,6 +20,50 @@
|
|
|
20
20
|
* directly above). Sites that build their regex from `PHASE_NUMBER_TOKEN_SOURCE`
|
|
21
21
|
* carry no literal grammar and pass automatically.
|
|
22
22
|
*
|
|
23
|
+
* #4634 extends the same pattern with two more detectors:
|
|
24
|
+
*
|
|
25
|
+
* - name-validity-guard drift: `hasNameableContent(s)` in `src/roadmap-parser.cts`
|
|
26
|
+
* is the sole owner of the "does this string have nameable content" predicate
|
|
27
|
+
* (`/[\p{L}\p{N}]/u.test(s)`). Any other `src/**` file re-deriving that exact
|
|
28
|
+
* character class (regex-literal or `new RegExp` template form) instead of
|
|
29
|
+
* calling `hasNameableContent` is drift, sanctioned the same way as the token
|
|
30
|
+
* and bracket rules (`// phase-id-owner:` on the nearest preceding non-blank
|
|
31
|
+
* line), with a line-level escape for a line that already calls
|
|
32
|
+
* `hasNameableContent(`.
|
|
33
|
+
*
|
|
34
|
+
* - shell phase-number-arithmetic ban: `$((10#...))` base-10-forced arithmetic
|
|
35
|
+
* inside `gsd-core/workflows/**\/*.md` and `gsd-core/references/**\/*.md` breaks
|
|
36
|
+
* on decimal or multi-segment phase ids and is banned outright. This scan runs
|
|
37
|
+
* over markdown, not `.cts` source, so its sanction is an HTML comment on the
|
|
38
|
+
* nearest preceding non-blank line: `<!-- phase-id-owner: <reason> -->`.
|
|
39
|
+
*
|
|
40
|
+
* - phase-heading-scan literal drift (#4906 Phase 5 / #4984): `findPhaseHeadingScanLiteralDrift`
|
|
41
|
+
* detects a hand-rolled `#{2,4}\s*Phase\s+` heading-SCAN literal, which bypasses
|
|
42
|
+
* `buildPhaseHeadingScanRegex` / `phaseHeadingPrefixSrcFor` (both in
|
|
43
|
+
* `src/phase-id.cts`) and so silently fails to recognize bracket-convention
|
|
44
|
+
* headings. Sanctioned the same way as the token/bracket/name-validity rules
|
|
45
|
+
* (`// phase-id-owner:` on the nearest preceding non-blank line), with a
|
|
46
|
+
* line-level escape for a line that already names either owner function.
|
|
47
|
+
* Wired into `scanRepo` (ADR-4910 §8): every pre-existing site this phase
|
|
48
|
+
* could not migrate (src/phase.cts x11, src/roadmap-parser.cts x3,
|
|
49
|
+
* src/planning-snapshot.cts x1) is grandfathered with a dedicated
|
|
50
|
+
* `// phase-id-owner:` sanction comment rather than a separate allowlist —
|
|
51
|
+
* the guard is ACTIVE from this phase forward: a fresh unsanctioned copy
|
|
52
|
+
* fails CI immediately, and each grandfathered comment is deleted as its
|
|
53
|
+
* site migrates in a later phase (ADR-4910 §8's census/grandfather/delete
|
|
54
|
+
* sequence, never renewed).
|
|
55
|
+
*
|
|
56
|
+
* - branch-slug fallback drift: a `.replace('{slug}', ... || 'phase')` call
|
|
57
|
+
* silently substitutes the literal string `'phase'` when a phase's slug
|
|
58
|
+
* can't be derived, producing a non-identifying branch name like
|
|
59
|
+
* `gsd/phase-08-phase` (#4126, now fixed via the shared renderPhaseBranchName
|
|
60
|
+
* owner in phase-id.cts, consumed by both prior call sites). Sanctioned
|
|
61
|
+
* the same way as the token/bracket/name-validity rules (`// phase-id-owner:`
|
|
62
|
+
* on the nearest preceding non-blank line), with a line-level escape for a
|
|
63
|
+
* line that already calls `renderPhaseBranchName(`. Unlike the other
|
|
64
|
+
* `.cts`-scanning rules, this one has no per-file exemption — it is a banned
|
|
65
|
+
* anti-pattern everywhere, not a grammar with one legitimate owner site.
|
|
66
|
+
*
|
|
23
67
|
* Detection is intentionally NARROW: only the contiguous canonical token
|
|
24
68
|
* (`\d+[A-Z]?(?:\.\d+)*`, its `[A-Za-z]` and `[.-]` near-variants, in both
|
|
25
69
|
* regex-literal `\d` and `new RegExp` template `\\d` escaping) is drift. Bare
|
|
@@ -82,6 +126,18 @@ const BRACKET_CODE_DRIFT_RE = /\[A-Z(?:a-z)?\]\[A-Z(?:a-z)?0-9_\]\*/;
|
|
|
82
126
|
const BRACKET_OWNER_HINT =
|
|
83
127
|
'BRACKET_PROJECT_CODE_SRC / BRACKET_ID_SRC / bracketMilestoneIntroSrcFor / BRACKET_MILESTONE_INTRO_CAPTURING_SRC';
|
|
84
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Pure: true if the nearest preceding non-blank line to `lines[i]` is a
|
|
131
|
+
* dedicated sanction comment matching `ownerRe`. Shared by every detector in
|
|
132
|
+
* this file so the "how do you sanction a finding" walk has one owner instead
|
|
133
|
+
* of four independent copies that could silently diverge.
|
|
134
|
+
*/
|
|
135
|
+
function isSanctionedByPrecedingComment(lines, i, ownerRe) {
|
|
136
|
+
let j = i - 1;
|
|
137
|
+
while (j >= 0 && lines[j].trim() === '') j--; // nearest preceding non-blank line
|
|
138
|
+
return j >= 0 && ownerRe.test(lines[j]);
|
|
139
|
+
}
|
|
140
|
+
|
|
85
141
|
/**
|
|
86
142
|
* Pure: find every literal re-derivation of the canonical phase-number token in
|
|
87
143
|
* `text` that is NOT sanctioned. A site is sanctioned when the nearest preceding
|
|
@@ -100,9 +156,7 @@ function findPhaseIdRegexDrift(text) {
|
|
|
100
156
|
const m = TOKEN_DRIFT_RE.exec(line);
|
|
101
157
|
if (!m) continue;
|
|
102
158
|
if (line.includes(CANON_REF)) continue;
|
|
103
|
-
|
|
104
|
-
while (j >= 0 && lines[j].trim() === '') j--; // nearest preceding non-blank line
|
|
105
|
-
if (j >= 0 && OWNER_RE.test(lines[j])) continue;
|
|
159
|
+
if (isSanctionedByPrecedingComment(lines, i, OWNER_RE)) continue;
|
|
106
160
|
out.push({ line: i + 1, found: m[0] });
|
|
107
161
|
}
|
|
108
162
|
return out;
|
|
@@ -122,21 +176,483 @@ function findBracketGrammarDrift(text) {
|
|
|
122
176
|
for (let i = 0; i < lines.length; i++) {
|
|
123
177
|
const m = BRACKET_CODE_DRIFT_RE.exec(lines[i]);
|
|
124
178
|
if (!m) continue;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
179
|
+
if (isSanctionedByPrecedingComment(lines, i, OWNER_RE)) continue;
|
|
180
|
+
out.push({ line: i + 1, found: m[0] });
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// #4634: the name-validity-guard grammar — `hasNameableContent(s)` in
|
|
186
|
+
// `src/roadmap-parser.cts` is `/[\p{L}\p{N}]/u.test(s)`. Tolerates both the
|
|
187
|
+
// regex-literal single-backslash form and the doubled-backslash template
|
|
188
|
+
// form (`new RegExp('[\\p{L}\\p{N}]'`), mirroring how TOKEN_DRIFT_RE tolerates
|
|
189
|
+
// both escapings.
|
|
190
|
+
const NAME_VALIDITY_DRIFT_RE = /\[\\{1,2}p\{L\}\\{1,2}p\{N\}\]/;
|
|
191
|
+
const NAME_VALIDITY_CANON_REF = 'hasNameableContent(';
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Pure: find every literal re-derivation of the canonical name-validity
|
|
195
|
+
* character class in `text` that is NOT sanctioned. Same sanction mechanism
|
|
196
|
+
* as the token/bracket rules — a dedicated `// phase-id-owner:` comment on
|
|
197
|
+
* the nearest preceding non-blank line — plus a line-level escape for a line
|
|
198
|
+
* that already calls `hasNameableContent(`. Returns [{ line, found }].
|
|
199
|
+
*/
|
|
200
|
+
function findNameValidityDrift(text) {
|
|
201
|
+
const out = [];
|
|
202
|
+
const lines = text.split('\n');
|
|
203
|
+
for (let i = 0; i < lines.length; i++) {
|
|
204
|
+
const line = lines[i];
|
|
205
|
+
const m = NAME_VALIDITY_DRIFT_RE.exec(line);
|
|
206
|
+
if (!m) continue;
|
|
207
|
+
if (line.includes(NAME_VALIDITY_CANON_REF)) continue;
|
|
208
|
+
if (isSanctionedByPrecedingComment(lines, i, OWNER_RE)) continue;
|
|
209
|
+
out.push({ line: i + 1, found: m[0] });
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// #4634: the branch-slug fallback anti-pattern (#4126) — a
|
|
215
|
+
// `.replace('{slug}', ... || 'phase')` call silently falls back to the
|
|
216
|
+
// literal string `'phase'` when a phase's slug can't be derived, producing a
|
|
217
|
+
// non-identifying branch name like `gsd/phase-08-phase`. Now fixed at both
|
|
218
|
+
// prior call sites (commands.cts, init.cts) via the shared
|
|
219
|
+
// `renderPhaseBranchName` owner in phase-id.cts; this rule is the ratchet
|
|
220
|
+
// against a THIRD site reintroducing the inline fallback. Deliberately
|
|
221
|
+
// narrow: it requires the literal `'phase'` fallback on the same line as the
|
|
222
|
+
// `{slug}` template token, so it does NOT match the sibling milestone-branch
|
|
223
|
+
// fallback (`|| 'milestone'`), which is a different, correct-as-is case.
|
|
224
|
+
const BRANCH_SLUG_FALLBACK_DRIFT_RE = /\{slug\}'.*\|\|\s*'phase'/;
|
|
225
|
+
|
|
226
|
+
// The canonical fix is `renderPhaseBranchName(...)`. There is no "owner file"
|
|
227
|
+
// for this rule the way there is for the token/bracket/name-validity
|
|
228
|
+
// grammars above — it is a banned anti-pattern everywhere, so no per-file
|
|
229
|
+
// exemption exists.
|
|
230
|
+
const BRANCH_SLUG_FALLBACK_CANON_REF = 'renderPhaseBranchName(';
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Pure: find every unsanctioned branch-slug `|| 'phase'` fallback in `text`.
|
|
234
|
+
* Same sanction mechanism as the token/bracket/name-validity rules — a
|
|
235
|
+
* dedicated `// phase-id-owner:` comment on the nearest preceding non-blank
|
|
236
|
+
* line — plus a line-level escape for a line that already calls
|
|
237
|
+
* `renderPhaseBranchName(`. Returns [{ line, found }].
|
|
238
|
+
*/
|
|
239
|
+
function findBranchSlugFallbackDrift(text) {
|
|
240
|
+
const out = [];
|
|
241
|
+
const lines = text.split('\n');
|
|
242
|
+
for (let i = 0; i < lines.length; i++) {
|
|
243
|
+
const line = lines[i];
|
|
244
|
+
const m = BRANCH_SLUG_FALLBACK_DRIFT_RE.exec(line);
|
|
245
|
+
if (!m) continue;
|
|
246
|
+
if (line.includes(BRANCH_SLUG_FALLBACK_CANON_REF)) continue;
|
|
247
|
+
if (isSanctionedByPrecedingComment(lines, i, OWNER_RE)) continue;
|
|
248
|
+
out.push({ line: i + 1, found: m[0] });
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// #4906 Phase 5 (#4984): a hand-rolled heading-SCAN literal `#{2,4}\s*Phase\s+`
|
|
254
|
+
// (in either regex-literal `\s` or template `\\s` escaping, mirroring
|
|
255
|
+
// TOKEN_DRIFT_RE's tolerance) bypasses `buildPhaseHeadingScanRegex` /
|
|
256
|
+
// `phaseHeadingPrefixSrcFor` (both in `src/phase-id.cts`) entirely, so it
|
|
257
|
+
// silently fails to recognize bracket-convention headings (`[GSD.03] Phase
|
|
258
|
+
// 03:`) — the exact defect this phase fixed at five call sites in init.cts
|
|
259
|
+
// and milestone.cts. A line naming either owner function counts as "built
|
|
260
|
+
// from the owner" and is not flagged, mirroring the token rule's
|
|
261
|
+
// `line.includes(CANON_REF)` escape.
|
|
262
|
+
const PHASE_HEADING_SCAN_LITERAL_DRIFT_RE = /#\{2,4\}\\{1,2}s\*Phase\\{1,2}s\+/;
|
|
263
|
+
const PHASE_HEADING_SCAN_LITERAL_CANON_REFS = ['buildPhaseHeadingScanRegex', 'phaseHeadingPrefixSrcFor'];
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Pure: find every unsanctioned hand-rolled `#{2,4}\s*Phase\s+` heading-scan
|
|
267
|
+
* literal in `text`. Sanctioned when the line already names either owner
|
|
268
|
+
* function (`buildPhaseHeadingScanRegex(` / `phaseHeadingPrefixSrcFor(`), or
|
|
269
|
+
* by a `// phase-id-owner:` comment on the nearest preceding non-blank line
|
|
270
|
+
* (same mechanism as every other rule in this file). Returns [{ line, found }].
|
|
271
|
+
*/
|
|
272
|
+
function findPhaseHeadingScanLiteralDrift(text) {
|
|
273
|
+
const out = [];
|
|
274
|
+
const lines = text.split('\n');
|
|
275
|
+
for (let i = 0; i < lines.length; i++) {
|
|
276
|
+
const line = lines[i];
|
|
277
|
+
const m = PHASE_HEADING_SCAN_LITERAL_DRIFT_RE.exec(line);
|
|
278
|
+
if (!m) continue;
|
|
279
|
+
if (PHASE_HEADING_SCAN_LITERAL_CANON_REFS.some((ref) => line.includes(ref))) continue;
|
|
280
|
+
if (isSanctionedByPrecedingComment(lines, i, OWNER_RE)) continue;
|
|
281
|
+
out.push({ line: i + 1, found: m[0] });
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// #4634: ban base-10-forced shell arithmetic (`$((10#...))`) on a variable
|
|
287
|
+
// that still carries a possibly-decimal/multi-segment phase id — this
|
|
288
|
+
// construct is exactly the pattern that breaks on a value like `08.5`. The
|
|
289
|
+
// capture group grabs the token immediately inside the parens (after an
|
|
290
|
+
// optional `$` and/or `{`, stripping a trailing `}`) so callers can inspect
|
|
291
|
+
// *which* variable is being coerced, not merely that the substring occurred.
|
|
292
|
+
//
|
|
293
|
+
// Refined post-#4619: the original blunt "ban `$((10#` outright" version
|
|
294
|
+
// over-fired on three false-positive classes once #4619's fix landed:
|
|
295
|
+
// 1. Prose mentioning the literal pattern in a full-line `#`-comment
|
|
296
|
+
// (filtered by the caller, not this regex — see below).
|
|
297
|
+
// 2. `$((10#$PHASE_INT))` / `$((10#$SPOT_PHASE_INT))` — arithmetic on the
|
|
298
|
+
// NOW-safe variable the #4619 fix produces via a leading-digit-run split
|
|
299
|
+
// (`PHASE_INT=${PHASE_NUMBER%%[!0-9]*}` since #4748; `%%.*` before it);
|
|
300
|
+
// a digit-run value can never contain a dot OR a letter, so base-10
|
|
301
|
+
// arithmetic on it can never hit the #4619 / #4748 error classes. Any
|
|
302
|
+
// name ending in `_INT` (case-insensitive) is that established "already
|
|
303
|
+
// reduced to a safe integer" convention. That convention is a NAME, not
|
|
304
|
+
// a proof — `findDotOnlyIntegerSplitDrift` below polices that the split
|
|
305
|
+
// producing it actually stops at the first non-digit.
|
|
306
|
+
// 3. `$((10#{plan_padded}))` / `$((10#${PLAN_ID}))` — plan ids are plain
|
|
307
|
+
// integers and were never in scope; this rule only polices variables
|
|
308
|
+
// that carry a *phase* id.
|
|
309
|
+
// So a match is only a violation when the captured name contains `phase`
|
|
310
|
+
// case-insensitively (it is phase-carrying) AND does not end in `_int`
|
|
311
|
+
// case-insensitively (it has not already been reduced to a safe integer).
|
|
312
|
+
const SHELL_PHASE_ARITH_DRIFT_RE = /\$\(\(\s*10#\$?\{?([A-Za-z0-9_]+)\}?/;
|
|
313
|
+
|
|
314
|
+
// A markdown comment can't easily carry a `//` line, so the sanction for the
|
|
315
|
+
// shell-arithmetic rule is an HTML comment on the nearest preceding non-blank
|
|
316
|
+
// line: `<!-- phase-id-owner: <reason> -->`.
|
|
317
|
+
const MD_OWNER_RE = /^\s*<!--.*phase-id-owner:/;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Pure: find every unsanctioned `$((10#...))` base-10-forced shell arithmetic
|
|
321
|
+
* site in `text` that still coerces an un-reduced phase-carrying variable.
|
|
322
|
+
* Skips full-line `#` comments outright (pure prose mentioning the pattern,
|
|
323
|
+
* not executable code), and skips any captured variable name that either
|
|
324
|
+
* doesn't contain `phase` (never in scope — e.g. plan ids) or already ends
|
|
325
|
+
* in `_int` (the #4619-fix convention for "safely stripped to an integer").
|
|
326
|
+
* Sanctioned by an HTML comment `<!-- phase-id-owner: ... -->` on the
|
|
327
|
+
* nearest preceding non-blank line. Returns [{ line, found }].
|
|
328
|
+
*/
|
|
329
|
+
function findShellPhaseArithDrift(text) {
|
|
330
|
+
const out = [];
|
|
331
|
+
const lines = text.split('\n');
|
|
332
|
+
for (let i = 0; i < lines.length; i++) {
|
|
333
|
+
const line = lines[i];
|
|
334
|
+
if (/^\s*#/.test(line)) continue;
|
|
335
|
+
const m = SHELL_PHASE_ARITH_DRIFT_RE.exec(line);
|
|
336
|
+
if (!m) continue;
|
|
337
|
+
const name = m[1];
|
|
338
|
+
if (!/phase/i.test(name)) continue;
|
|
339
|
+
if (/_int$/i.test(name)) continue;
|
|
340
|
+
if (isSanctionedByPrecedingComment(lines, i, MD_OWNER_RE)) continue;
|
|
341
|
+
out.push({ line: i + 1, found: m[0] });
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// #4634: the markdown scan roots — shell embedded in workflow/reference docs.
|
|
347
|
+
const MD_SCAN_DIRS = [path.join('gsd-core', 'workflows'), path.join('gsd-core', 'references')];
|
|
348
|
+
|
|
349
|
+
// #4568 (epic #4634): the single-segment phase regex ban scans a THIRD root,
|
|
350
|
+
// `agents/**/*.md`, that the #4619 shell-arithmetic extension above never
|
|
351
|
+
// touched — the gsd-code-fixer agent prompts re-derive the phase-number
|
|
352
|
+
// grammar too. Reuses the same `walkMd` walker as the shell-arith scan.
|
|
353
|
+
const SINGLE_SEGMENT_SCAN_DIRS = [...MD_SCAN_DIRS, 'agents'];
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Scan `gsd-core/workflows/**\/*.md` and `gsd-core/references/**\/*.md` for
|
|
357
|
+
* unsanctioned `$((10#...))` shell arithmetic. Returns [{ file, line, found }]
|
|
358
|
+
* with repo-relative paths.
|
|
359
|
+
*/
|
|
360
|
+
function scanMarkdownShellArith(root) {
|
|
361
|
+
const violations = [];
|
|
362
|
+
for (const dir of MD_SCAN_DIRS) {
|
|
363
|
+
for (const file of walkMd(path.join(root, dir), [])) {
|
|
364
|
+
const rel = path.relative(root, file);
|
|
365
|
+
let text;
|
|
366
|
+
try {
|
|
367
|
+
text = fs.readFileSync(file, 'utf8');
|
|
368
|
+
} catch {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
for (const d of findShellPhaseArithDrift(text)) {
|
|
372
|
+
violations.push({ file: rel, kind: 'shell-arith', ...d });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return violations;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// #4568 (epic #4634): ban the single-optional-dotted-segment phase regex
|
|
380
|
+
// shape `[0-9]+(\.[0-9]+)?` (and its `\d`/doubled-backslash near-variants)
|
|
381
|
+
// outright — this is exactly the grammar that hard-rejects or silently
|
|
382
|
+
// truncates a 3-or-more-segment phase id like `23.1.2`. The canonical
|
|
383
|
+
// grammar (`src/phase-id.cts`) uses the unbounded `(?:\.\d+)*` form; shell
|
|
384
|
+
// snippets embedded in markdown can't import that module, so textual parity
|
|
385
|
+
// (`*` in place of `?`) is the fix, and this rule is the ratchet against a
|
|
386
|
+
// future site re-deriving the bounded form. Deliberately narrow to the
|
|
387
|
+
// bounded ONE-optional-segment shape — the fixed `*`-form is not flagged.
|
|
388
|
+
const SINGLE_SEGMENT_PHASE_DRIFT_RE =
|
|
389
|
+
/(?:\\{1,2}d|\[0-9\])\+\(\\{1,2}\.(?:\\{1,2}d|\[0-9\])\+\)\?/;
|
|
390
|
+
|
|
391
|
+
// A single-segment shape like `[0-9]+(\.[0-9]+)?` is not inherently
|
|
392
|
+
// phase-specific (e.g. it could describe a version number), so the rule
|
|
393
|
+
// only fires on a line whose text plausibly carries a phase-number
|
|
394
|
+
// variable — a case-insensitive `phase` substring anywhere on the line,
|
|
395
|
+
// mirroring the phase-carrying filter `findShellPhaseArithDrift` already
|
|
396
|
+
// applies to its own variable-name capture.
|
|
397
|
+
const PHASE_CARRYING_LINE_RE = /phase/i;
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Pure: find every unsanctioned single-optional-dotted-segment phase regex
|
|
401
|
+
* in `text`, restricted to lines that plausibly carry a phase-number
|
|
402
|
+
* variable. Sanctioned by an HTML comment `<!-- phase-id-owner: ... -->` on
|
|
403
|
+
* the nearest preceding non-blank line (same convention as the shell-arith
|
|
404
|
+
* rule). Returns [{ line, found }].
|
|
405
|
+
*/
|
|
406
|
+
function findSingleSegmentPhaseRegexDrift(text) {
|
|
407
|
+
const out = [];
|
|
408
|
+
const lines = text.split('\n');
|
|
409
|
+
for (let i = 0; i < lines.length; i++) {
|
|
410
|
+
const line = lines[i];
|
|
411
|
+
const m = SINGLE_SEGMENT_PHASE_DRIFT_RE.exec(line);
|
|
412
|
+
if (!m) continue;
|
|
413
|
+
if (!PHASE_CARRYING_LINE_RE.test(line)) continue;
|
|
414
|
+
if (isSanctionedByPrecedingComment(lines, i, MD_OWNER_RE)) continue;
|
|
415
|
+
out.push({ line: i + 1, found: m[0] });
|
|
416
|
+
}
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Scan `gsd-core/workflows/**\/*.md`, `gsd-core/references/**\/*.md`, and
|
|
422
|
+
* `agents/**\/*.md` for unsanctioned single-optional-dotted-segment phase
|
|
423
|
+
* regexes. Returns [{ file, line, found }] with repo-relative paths.
|
|
424
|
+
*/
|
|
425
|
+
function scanMarkdownSingleSegmentPhaseRegex(root) {
|
|
426
|
+
const violations = [];
|
|
427
|
+
for (const dir of SINGLE_SEGMENT_SCAN_DIRS) {
|
|
428
|
+
for (const file of walkMd(path.join(root, dir), [])) {
|
|
429
|
+
const rel = path.relative(root, file);
|
|
430
|
+
let text;
|
|
431
|
+
try {
|
|
432
|
+
text = fs.readFileSync(file, 'utf8');
|
|
433
|
+
} catch {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
for (const d of findSingleSegmentPhaseRegexDrift(text)) {
|
|
437
|
+
violations.push({ file: rel, kind: 'single-segment-phase-regex', ...d });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return violations;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// #4660 (epic #4634): the six shell/markdown mirrors #4568 widened on the
|
|
445
|
+
// segment-count axis stayed digit-only on the LETTER axis — the canonical
|
|
446
|
+
// grammar (`src/phase-id.cts`) is `\d+[A-Z]?(?:\.\d+)*`, with an optional
|
|
447
|
+
// single uppercase letter after the leading digits (`12A`, `3A`, `23A.1.2`,
|
|
448
|
+
// documented in docs/CONFIGURATION.md and relied on by `renameIntegerPhases`).
|
|
449
|
+
// A digit-only mirror `[0-9]+(\.[0-9]+)*` hard-rejects (validating sites) or
|
|
450
|
+
// silently truncates (extracting sites) a letter-suffixed id. This rule is the
|
|
451
|
+
// ratchet for that axis, the twin of the single-segment rule above: it flags
|
|
452
|
+
// the unbounded-segment shape whose digit run is NOT followed by the letter
|
|
453
|
+
// class. `[A-Z]` is the canonical spelling; the case-flexible `[A-Za-z]`
|
|
454
|
+
// directory-scanning variant is a deliberately separate axis and is tolerated
|
|
455
|
+
// here so this rule cannot force it to narrow.
|
|
456
|
+
const LETTERLESS_PHASE_MIRROR_DRIFT_RE =
|
|
457
|
+
/(?:\\{1,2}d|\[0-9\])\+(?!\[A-Z(?:a-z)?\]\?)\(\\{1,2}\.(?:\\{1,2}d|\[0-9\])\+\)\*/;
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Pure: find every unsanctioned digit-only (letter-less) unbounded-segment
|
|
461
|
+
* phase regex in `text`, restricted to lines that plausibly carry a
|
|
462
|
+
* phase-number variable — the same `PHASE_CARRYING_LINE_RE` filter and the
|
|
463
|
+
* same `<!-- phase-id-owner: ... -->` sanction as the single-segment rule.
|
|
464
|
+
* Returns [{ line, found }].
|
|
465
|
+
*/
|
|
466
|
+
function findLetterlessPhaseMirrorDrift(text) {
|
|
467
|
+
const out = [];
|
|
468
|
+
const lines = text.split('\n');
|
|
469
|
+
for (let i = 0; i < lines.length; i++) {
|
|
470
|
+
const line = lines[i];
|
|
471
|
+
const m = LETTERLESS_PHASE_MIRROR_DRIFT_RE.exec(line);
|
|
472
|
+
if (!m) continue;
|
|
473
|
+
if (!PHASE_CARRYING_LINE_RE.test(line)) continue;
|
|
474
|
+
if (isSanctionedByPrecedingComment(lines, i, MD_OWNER_RE)) continue;
|
|
475
|
+
out.push({ line: i + 1, found: m[0] });
|
|
476
|
+
}
|
|
477
|
+
return out;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Scan the same three markdown roots as the single-segment rule for
|
|
482
|
+
* unsanctioned letter-less phase-regex mirrors. Returns [{ file, line, found }]
|
|
483
|
+
* with repo-relative paths.
|
|
484
|
+
*/
|
|
485
|
+
function scanMarkdownLetterlessPhaseMirror(root) {
|
|
486
|
+
const violations = [];
|
|
487
|
+
for (const dir of SINGLE_SEGMENT_SCAN_DIRS) {
|
|
488
|
+
for (const file of walkMd(path.join(root, dir), [])) {
|
|
489
|
+
const rel = path.relative(root, file);
|
|
490
|
+
let text;
|
|
491
|
+
try {
|
|
492
|
+
text = fs.readFileSync(file, 'utf8');
|
|
493
|
+
} catch {
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
for (const d of findLetterlessPhaseMirrorDrift(text)) {
|
|
497
|
+
violations.push({ file: rel, kind: 'letterless-phase-mirror', ...d });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return violations;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// #4748 (epic #4634): three shell shapes OUTSIDE the grammar-mirror family the
|
|
505
|
+
// rules above police — consumers of a phase id rather than regexes for one —
|
|
506
|
+
// each of which the letter axis broke while every rule above reported clean:
|
|
507
|
+
//
|
|
508
|
+
// a. `PHASE_INT=${PHASE_NUMBER%%.*}` — the post-#4619 dot-only split. The
|
|
509
|
+
// `_INT` name it produces satisfies the shell-arithmetic rule's escape,
|
|
510
|
+
// but on `03A` the "integer" is `03A` and `$((10#03A))` aborts. The safe
|
|
511
|
+
// split stops at the first NON-digit: `${PHASE_NUMBER%%[!0-9]*}`.
|
|
512
|
+
// b. `[0-9]+\.?[0-9]*` (and `\d+\.?\d*`) — a digit-then-optional-dot
|
|
513
|
+
// extraction that is neither the bounded `(\.[0-9]+)?` shape the
|
|
514
|
+
// single-segment rule bans nor the unbounded `(\.[0-9]+)*` shape the
|
|
515
|
+
// letterless rule inspects, so both were blind to it. It captures `12`
|
|
516
|
+
// from `12A` and `23.1` from `23.1.2`, silently.
|
|
517
|
+
// c. `printf "%02d" "$PHASE_NUMBER"` — re-padding the whole id in shell.
|
|
518
|
+
// Rejects a letter id (prints `03`, exit 1) and misreads an already
|
|
519
|
+
// padded `08` as octal (prints `00`). Padding belongs to the canonical
|
|
520
|
+
// normalizer (`padded_phase` from init, or `normalizePhaseName`); the one
|
|
521
|
+
// legitimate shell pad is of an `_INT` value via `$((10#…))`.
|
|
522
|
+
//
|
|
523
|
+
// Same sanction as the other markdown rules: `<!-- phase-id-owner: … -->` on
|
|
524
|
+
// the nearest preceding non-blank line. Same documented limit: a per-line
|
|
525
|
+
// textual scan for the common accidental shape, not an obfuscated one.
|
|
526
|
+
|
|
527
|
+
// a. `<name>_INT=${<phase-carrying>%%.*}` — the `_INT` destination is the
|
|
528
|
+
// discriminator, deliberately: it is the name the shell-arithmetic rule
|
|
529
|
+
// trusts as "already a safe integer", so a dot-only split INTO it is the
|
|
530
|
+
// exact promise this rule exists to check. A dot-only split into any other
|
|
531
|
+
// name is a different, legitimate operation — `PARENT_PHASE="${PHASE_NUMBER%%.*}"`
|
|
532
|
+
// (gap-closure-artifacts.md) wants everything before the first dot, letter
|
|
533
|
+
// included, and is correct. Widening to any destination was tried and flagged
|
|
534
|
+
// that site. The optional quote after `=` is the one spelling that site uses.
|
|
535
|
+
const DOT_ONLY_INT_SPLIT_DRIFT_RE = /[A-Za-z0-9_]*_INT="?\$\{([A-Za-z0-9_]+)%%\\?\.\*\}/i;
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Pure: find every unsanctioned dot-only integer split of a phase-carrying
|
|
539
|
+
* variable in `text`. Skips full-line `#` comments (prose). Returns [{ line, found }].
|
|
540
|
+
*/
|
|
541
|
+
function findDotOnlyIntegerSplitDrift(text) {
|
|
542
|
+
const out = [];
|
|
543
|
+
const lines = text.split('\n');
|
|
544
|
+
for (let i = 0; i < lines.length; i++) {
|
|
545
|
+
const line = lines[i];
|
|
546
|
+
if (/^\s*#/.test(line)) continue;
|
|
547
|
+
const m = DOT_ONLY_INT_SPLIT_DRIFT_RE.exec(line);
|
|
548
|
+
if (!m) continue;
|
|
549
|
+
if (!/phase/i.test(m[1])) continue;
|
|
550
|
+
if (isSanctionedByPrecedingComment(lines, i, MD_OWNER_RE)) continue;
|
|
551
|
+
out.push({ line: i + 1, found: m[0] });
|
|
552
|
+
}
|
|
553
|
+
return out;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// b. The digit-then-optional-dot shape, in both `[0-9]` and `\d` spellings.
|
|
557
|
+
// Anchored on the trailing `*` of the second digit class so a bare `[0-9]+`
|
|
558
|
+
// probe or a `[0-9]+\.[0-9]+` (mandatory-dot) shape is not matched.
|
|
559
|
+
const LOOSE_DOTTED_PHASE_DRIFT_RE = /(?:\\{1,2}d|\[0-9\])\+\\{1,2}\.\?(?:\\{1,2}d|\[0-9\])\*/;
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Pure: find every unsanctioned `[0-9]+\.?[0-9]*`-shaped phase extraction in
|
|
563
|
+
* `text`, restricted to phase-carrying lines like its two sibling regex rules.
|
|
564
|
+
* Returns [{ line, found }].
|
|
565
|
+
*/
|
|
566
|
+
function findLooseDottedPhaseRegexDrift(text) {
|
|
567
|
+
const out = [];
|
|
568
|
+
const lines = text.split('\n');
|
|
569
|
+
for (let i = 0; i < lines.length; i++) {
|
|
570
|
+
const line = lines[i];
|
|
571
|
+
const m = LOOSE_DOTTED_PHASE_DRIFT_RE.exec(line);
|
|
572
|
+
if (!m) continue;
|
|
573
|
+
if (!PHASE_CARRYING_LINE_RE.test(line)) continue;
|
|
574
|
+
if (isSanctionedByPrecedingComment(lines, i, MD_OWNER_RE)) continue;
|
|
128
575
|
out.push({ line: i + 1, found: m[0] });
|
|
129
576
|
}
|
|
130
577
|
return out;
|
|
131
578
|
}
|
|
132
579
|
|
|
580
|
+
// c. `printf "%Nd" …` / `printf '%0Nd' …` — any integer conversion, either
|
|
581
|
+
// quote, with or without the zero flag — whose argument list names a
|
|
582
|
+
// phase-carrying variable that is NOT an `_INT` (the `$((10#$PHASE_INT))`
|
|
583
|
+
// pad is the sanctioned shape). `%d` cannot parse a letter id under any
|
|
584
|
+
// width, so the flag is not the discriminator. Captures the first such name
|
|
585
|
+
// so the report says what was padded.
|
|
586
|
+
const SHELL_PHASE_PRINTF_PAD_RE = /printf\s+(?:"%0?\d*d[^"]*"|'%0?\d*d[^']*')\s+(.*)$/;
|
|
587
|
+
const SHELL_VAR_NAME_RE = /\$\{?([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Pure: find every unsanctioned `printf "%02d"` re-pad of a phase-carrying,
|
|
591
|
+
* non-`_INT` shell variable in `text`. Skips full-line `#` comments.
|
|
592
|
+
* Returns [{ line, found }].
|
|
593
|
+
*/
|
|
594
|
+
function findShellPhasePrintfPadDrift(text) {
|
|
595
|
+
const out = [];
|
|
596
|
+
const lines = text.split('\n');
|
|
597
|
+
for (let i = 0; i < lines.length; i++) {
|
|
598
|
+
const line = lines[i];
|
|
599
|
+
if (/^\s*#/.test(line)) continue;
|
|
600
|
+
const m = SHELL_PHASE_PRINTF_PAD_RE.exec(line);
|
|
601
|
+
if (!m) continue;
|
|
602
|
+
const offender = [...m[1].matchAll(SHELL_VAR_NAME_RE)]
|
|
603
|
+
.map((v) => v[1])
|
|
604
|
+
.find((name) => /phase/i.test(name) && !/_int$/i.test(name));
|
|
605
|
+
if (!offender) continue;
|
|
606
|
+
if (isSanctionedByPrecedingComment(lines, i, MD_OWNER_RE)) continue;
|
|
607
|
+
out.push({ line: i + 1, found: `printf "%0…d" …$${offender}` });
|
|
608
|
+
}
|
|
609
|
+
return out;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Scan the shell roots (`gsd-core/workflows/**\/*.md`, `gsd-core/references/**\/*.md`)
|
|
614
|
+
* for the two shell-idiom rules (a, c) and the three regex roots (those plus
|
|
615
|
+
* `agents/**\/*.md`) for the extraction-shape rule (b). Returns
|
|
616
|
+
* [{ file, kind, line, found }] with repo-relative paths.
|
|
617
|
+
*/
|
|
618
|
+
function scanMarkdownLetterAxisConsumers(root) {
|
|
619
|
+
const violations = [];
|
|
620
|
+
const collect = (dirs, finder, kind) => {
|
|
621
|
+
for (const dir of dirs) {
|
|
622
|
+
for (const file of walkMd(path.join(root, dir), [])) {
|
|
623
|
+
const rel = path.relative(root, file);
|
|
624
|
+
let text;
|
|
625
|
+
try {
|
|
626
|
+
text = fs.readFileSync(file, 'utf8');
|
|
627
|
+
} catch {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
for (const d of finder(text)) {
|
|
631
|
+
violations.push({ file: rel, kind, ...d });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
collect(MD_SCAN_DIRS, findDotOnlyIntegerSplitDrift, 'dot-only-int-split');
|
|
637
|
+
collect(SINGLE_SEGMENT_SCAN_DIRS, findLooseDottedPhaseRegexDrift, 'loose-dotted-phase-regex');
|
|
638
|
+
collect(MD_SCAN_DIRS, findShellPhasePrintfPadDrift, 'shell-phase-printf-pad');
|
|
639
|
+
return violations;
|
|
640
|
+
}
|
|
641
|
+
|
|
133
642
|
// Authored TypeScript source only (the generated bin/lib/*.cjs mirror it).
|
|
134
643
|
const SCAN_DIRS = ['src'];
|
|
135
644
|
const SCAN_EXT = new Set(['.cts', '.ts', '.mts']);
|
|
136
645
|
// The canonical owner defines the grammar; it is exempt by construction.
|
|
137
646
|
const EXEMPT = new Set([path.join('src', 'phase-id.cts')]);
|
|
138
647
|
|
|
139
|
-
|
|
648
|
+
// #4634: the name-validity-guard rule owns a DIFFERENT file (roadmap-parser.cts
|
|
649
|
+
// defines `hasNameableContent`), so it needs its own exemption set — the
|
|
650
|
+
// token/bracket rules above must NOT start exempting roadmap-parser.cts too,
|
|
651
|
+
// since it is not their owner.
|
|
652
|
+
const NAME_VALIDITY_EXEMPT = new Set([path.join('src', 'roadmap-parser.cts')]);
|
|
653
|
+
|
|
654
|
+
function walk(dir, acc, ext) {
|
|
655
|
+
const extSet = ext || SCAN_EXT;
|
|
140
656
|
let entries;
|
|
141
657
|
try {
|
|
142
658
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
@@ -147,14 +663,22 @@ function walk(dir, acc) {
|
|
|
147
663
|
const full = path.join(dir, entry.name);
|
|
148
664
|
if (entry.isDirectory()) {
|
|
149
665
|
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.git') continue;
|
|
150
|
-
walk(full, acc);
|
|
151
|
-
} else if (entry.isFile() &&
|
|
666
|
+
walk(full, acc, extSet);
|
|
667
|
+
} else if (entry.isFile() && extSet.has(path.extname(entry.name))) {
|
|
152
668
|
acc.push(full);
|
|
153
669
|
}
|
|
154
670
|
}
|
|
155
671
|
return acc;
|
|
156
672
|
}
|
|
157
673
|
|
|
674
|
+
// #4634: markdown scan for the shell phase-arithmetic ban walks a disjoint set
|
|
675
|
+
// of roots/extensions from the src/**/*.cts scan above, so it gets its own thin
|
|
676
|
+
// wrapper over the same `walk` rather than a parallel tree-walker.
|
|
677
|
+
const MD_EXT = new Set(['.md']);
|
|
678
|
+
function walkMd(dir, acc) {
|
|
679
|
+
return walk(dir, acc, MD_EXT);
|
|
680
|
+
}
|
|
681
|
+
|
|
158
682
|
// ─── #2761 M4: the heading-baseline selector census ────────────────────────
|
|
159
683
|
//
|
|
160
684
|
// `phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.<MODE>)` is the other half
|
|
@@ -239,23 +763,86 @@ function scanRepo(root) {
|
|
|
239
763
|
for (const d of findBracketGrammarDrift(text)) {
|
|
240
764
|
violations.push({ file: rel, kind: 'bracket', ...d });
|
|
241
765
|
}
|
|
766
|
+
// #4634: name-validity-guard drift, exempting only its own owner file.
|
|
767
|
+
if (!NAME_VALIDITY_EXEMPT.has(rel)) {
|
|
768
|
+
for (const d of findNameValidityDrift(text)) {
|
|
769
|
+
violations.push({ file: rel, kind: 'name-validity', ...d });
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
// #4634: branch-slug fallback anti-pattern, exempt nowhere.
|
|
773
|
+
for (const d of findBranchSlugFallbackDrift(text)) {
|
|
774
|
+
violations.push({ file: rel, kind: 'branch-slug-fallback', ...d });
|
|
775
|
+
}
|
|
776
|
+
// #4906 Phase 5 (#4984): heading-scan literal drift, owner file exempt
|
|
777
|
+
// via the shared `EXEMPT` check above. Pre-existing sites this phase
|
|
778
|
+
// could not reach (src/phase.cts, src/roadmap-parser.cts,
|
|
779
|
+
// src/planning-snapshot.cts) are grandfathered with a `// phase-id-owner:`
|
|
780
|
+
// sanction each, per ADR-4910 §8's census/grandfather/delete Phase 6 plan
|
|
781
|
+
// — the allowlist is those sanction comments, not a separate list here,
|
|
782
|
+
// and it is deleted entry-by-entry as each site migrates.
|
|
783
|
+
for (const d of findPhaseHeadingScanLiteralDrift(text)) {
|
|
784
|
+
violations.push({ file: rel, kind: 'phase-heading-scan-literal', ...d });
|
|
785
|
+
}
|
|
242
786
|
}
|
|
243
787
|
}
|
|
244
788
|
return violations;
|
|
245
789
|
}
|
|
246
790
|
|
|
791
|
+
/**
|
|
792
|
+
* Scan EVERYTHING this seam guards: the `src/**\/*.cts` token/bracket/
|
|
793
|
+
* name-validity invariants (`scanRepo`) plus the markdown shell
|
|
794
|
+
* phase-arithmetic ban (`scanMarkdownShellArith`). This is what the CLI runs;
|
|
795
|
+
* `scanRepo` alone stays narrowly scoped to its original src/** contract so
|
|
796
|
+
* a live, separately-tracked markdown defect (#4619) cannot make the
|
|
797
|
+
* pinned-clean `scanRepo` test spuriously fail.
|
|
798
|
+
*/
|
|
799
|
+
function scanAll(root) {
|
|
800
|
+
return [
|
|
801
|
+
...scanRepo(root),
|
|
802
|
+
...scanMarkdownShellArith(root),
|
|
803
|
+
...scanMarkdownSingleSegmentPhaseRegex(root),
|
|
804
|
+
...scanMarkdownLetterlessPhaseMirror(root),
|
|
805
|
+
...scanMarkdownLetterAxisConsumers(root),
|
|
806
|
+
];
|
|
807
|
+
}
|
|
808
|
+
|
|
247
809
|
function main() {
|
|
248
810
|
const root = path.join(__dirname, '..');
|
|
249
|
-
const violations =
|
|
811
|
+
const violations = scanAll(root);
|
|
250
812
|
if (violations.length === 0) {
|
|
251
|
-
process.stdout.write(
|
|
813
|
+
process.stdout.write(
|
|
814
|
+
'ok phase-id-drift: no unsanctioned phase-token, bracket-grammar, name-validity, ' +
|
|
815
|
+
'branch-slug-fallback, or shell phase-arithmetic re-derivations found\n',
|
|
816
|
+
);
|
|
252
817
|
return;
|
|
253
818
|
}
|
|
254
819
|
process.stderr.write('phase-id-drift: literal re-derivation(s) of a canonical grammar found.\n');
|
|
255
820
|
process.stderr.write(`Build the regex from phase-id.cjs \`${CANON_REF}\` (or phaseMarkdownRegexSource for a\n`);
|
|
256
821
|
process.stderr.write(`known number) for the phase-number token, or from ${BRACKET_OWNER_HINT}\n`);
|
|
257
|
-
process.stderr.write('for the bracket grammar
|
|
258
|
-
process.stderr.write('
|
|
822
|
+
process.stderr.write('for the bracket grammar, or call `hasNameableContent(` (src/roadmap-parser.cts) for\n');
|
|
823
|
+
process.stderr.write('the name-validity predicate — or sanction the site with a dedicated\n');
|
|
824
|
+
process.stderr.write('`// phase-id-owner: <reason>` comment on the line directly above the regex.\n');
|
|
825
|
+
process.stderr.write('`$((10#...))` base-10-forced shell arithmetic is banned outright in\n');
|
|
826
|
+
process.stderr.write('gsd-core/workflows/**/*.md and gsd-core/references/**/*.md — sanction with\n');
|
|
827
|
+
process.stderr.write('`<!-- phase-id-owner: <reason> -->` on the line directly above.\n');
|
|
828
|
+
process.stderr.write('The single-optional-dotted-segment phase regex `[0-9]+(\\.[0-9]+)?` (or its \\d\n');
|
|
829
|
+
process.stderr.write('near-variant) is banned outright in gsd-core/workflows/**/*.md,\n');
|
|
830
|
+
process.stderr.write('gsd-core/references/**/*.md, and agents/**/*.md — widen it to `*` (unbounded\n');
|
|
831
|
+
process.stderr.write('segments) or sanction with `<!-- phase-id-owner: <reason> -->`.\n');
|
|
832
|
+
process.stderr.write('A digit-only unbounded-segment phase regex `[0-9]+(\\.[0-9]+)*` on the same roots\n');
|
|
833
|
+
process.stderr.write('is missing the canonical letter axis (#4660) — widen to `[0-9]+[A-Z]?(\\.[0-9]+)*`\n');
|
|
834
|
+
process.stderr.write('or sanction with `<!-- phase-id-owner: <reason> -->`.\n');
|
|
835
|
+
process.stderr.write('Three letter-hostile consumers of a phase id are banned on the same roots (#4748):\n');
|
|
836
|
+
process.stderr.write('a dot-only integer split `X_INT=${PHASE%%.*}` (split at the first non-digit,\n');
|
|
837
|
+
process.stderr.write('`${PHASE%%[!0-9]*}`); a `[0-9]+\\.?[0-9]*` extraction (use `[0-9]+[A-Z]?(\\.[0-9]+)*`);\n');
|
|
838
|
+
process.stderr.write('and a `printf "%02d"` re-pad of a phase variable (bind init\'s `padded_phase`, or pad\n');
|
|
839
|
+
process.stderr.write('only an `_INT` via `$((10#…))`) — or sanction with `<!-- phase-id-owner: <reason> -->`.\n');
|
|
840
|
+
process.stderr.write('A `.replace(\'{slug}\', ... || \'phase\')` fallback is banned outright (#4126) —\n');
|
|
841
|
+
process.stderr.write('use `renderPhaseBranchName(` or sanction with\n');
|
|
842
|
+
process.stderr.write('`// phase-id-owner: <reason>` on the line directly above.\n');
|
|
843
|
+
process.stderr.write('A hand-rolled `#{2,4}\\s*Phase\\s+` heading-scan literal is banned (#4906 Phase 5) —\n');
|
|
844
|
+
process.stderr.write('use `buildPhaseHeadingScanRegex(` / `phaseHeadingPrefixSrcFor(` or sanction with\n');
|
|
845
|
+
process.stderr.write('`// phase-id-owner: <reason>` on the line directly above:\n');
|
|
259
846
|
for (const d of violations) {
|
|
260
847
|
process.stderr.write(` [${d.kind}] ${d.file}:${d.line} ${d.found}\n`);
|
|
261
848
|
}
|
|
@@ -267,9 +854,32 @@ if (require.main === module) main();
|
|
|
267
854
|
module.exports = {
|
|
268
855
|
findPhaseIdRegexDrift,
|
|
269
856
|
findBracketGrammarDrift,
|
|
857
|
+
findNameValidityDrift,
|
|
858
|
+
findBranchSlugFallbackDrift,
|
|
859
|
+
findPhaseHeadingScanLiteralDrift,
|
|
860
|
+
findShellPhaseArithDrift,
|
|
861
|
+
findSingleSegmentPhaseRegexDrift,
|
|
862
|
+
findLetterlessPhaseMirrorDrift,
|
|
863
|
+
findDotOnlyIntegerSplitDrift,
|
|
864
|
+
findLooseDottedPhaseRegexDrift,
|
|
865
|
+
findShellPhasePrintfPadDrift,
|
|
866
|
+
scanMarkdownShellArith,
|
|
867
|
+
scanMarkdownSingleSegmentPhaseRegex,
|
|
868
|
+
scanMarkdownLetterlessPhaseMirror,
|
|
869
|
+
scanMarkdownLetterAxisConsumers,
|
|
270
870
|
scanRepo,
|
|
871
|
+
scanAll,
|
|
271
872
|
countSelectorBaselines,
|
|
272
873
|
scanSelectorBaselines,
|
|
273
874
|
TOKEN_DRIFT_RE,
|
|
274
875
|
BRACKET_CODE_DRIFT_RE,
|
|
876
|
+
NAME_VALIDITY_DRIFT_RE,
|
|
877
|
+
BRANCH_SLUG_FALLBACK_DRIFT_RE,
|
|
878
|
+
PHASE_HEADING_SCAN_LITERAL_DRIFT_RE,
|
|
879
|
+
SHELL_PHASE_ARITH_DRIFT_RE,
|
|
880
|
+
SINGLE_SEGMENT_PHASE_DRIFT_RE,
|
|
881
|
+
LETTERLESS_PHASE_MIRROR_DRIFT_RE,
|
|
882
|
+
DOT_ONLY_INT_SPLIT_DRIFT_RE,
|
|
883
|
+
LOOSE_DOTTED_PHASE_DRIFT_RE,
|
|
884
|
+
SHELL_PHASE_PRINTF_PAD_RE,
|
|
275
885
|
};
|