@opengsd/gsd-core 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-debug-session-manager.md +11 -0
- package/agents/gsd-doc-synthesizer.md +2 -4
- package/agents/gsd-executor.md +5 -5
- package/agents/gsd-mempalace-curator.md +5 -2
- package/agents/gsd-phase-researcher.md +20 -1
- package/agents/gsd-plan-checker.md +37 -0
- package/agents/gsd-planner.md +44 -46
- package/agents/gsd-user-profiler.md +3 -0
- package/agents/gsd-verifier.md +12 -3
- package/bin/install.js +841 -971
- package/bin/lib/ui-safety-gate.cjs +2 -0
- package/commands/gsd/code-review.md +1 -1
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/map-codebase.md +1 -1
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/quick.md +1 -1
- package/commands/gsd/review-backlog.md +2 -1
- package/commands/gsd/verify-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +469 -88
- package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
- package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
- package/gsd-core/bin/lib/api-coverage.cjs +3 -5
- package/gsd-core/bin/lib/artifacts.cjs +3 -0
- package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
- package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
- package/gsd-core/bin/lib/audit.cjs +876 -240
- package/gsd-core/bin/lib/broken-windows.cjs +1 -1
- package/gsd-core/bin/lib/capability-consent.cjs +149 -15
- package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
- package/gsd-core/bin/lib/capability-registry.cjs +575 -101
- package/gsd-core/bin/lib/capability-source.cjs +92 -0
- package/gsd-core/bin/lib/capability-trust.cjs +444 -25
- package/gsd-core/bin/lib/capability-validator.cjs +495 -22
- package/gsd-core/bin/lib/capability-writer.cjs +3 -2
- package/gsd-core/bin/lib/check-command-router.cjs +71 -37
- package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
- package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
- package/gsd-core/bin/lib/command-aliases.cjs +22 -0
- package/gsd-core/bin/lib/command-roster.cjs +44 -1
- package/gsd-core/bin/lib/commands.cjs +651 -86
- package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
- package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
- package/gsd-core/bin/lib/config-loader.cjs +75 -0
- package/gsd-core/bin/lib/config.cjs +10 -1
- package/gsd-core/bin/lib/core-utils.cjs +127 -29
- package/gsd-core/bin/lib/decisions.cjs +23 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
- package/gsd-core/bin/lib/frontmatter.cjs +155 -20
- package/gsd-core/bin/lib/gap-checker.cjs +68 -7
- package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
- 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 +145 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
- package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
- package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
- package/gsd-core/bin/lib/init.cjs +321 -129
- package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
- package/gsd-core/bin/lib/install-engine.cjs +745 -258
- package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
- package/gsd-core/bin/lib/install-profiles.cjs +134 -57
- package/gsd-core/bin/lib/install-scope.cjs +270 -0
- package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
- package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
- package/gsd-core/bin/lib/io.cjs +10 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
- package/gsd-core/bin/lib/markdown-table.cjs +133 -20
- package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
- package/gsd-core/bin/lib/milestone.cjs +754 -70
- package/gsd-core/bin/lib/model-catalog.cjs +59 -1
- package/gsd-core/bin/lib/model-resolver.cjs +183 -40
- package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
- package/gsd-core/bin/lib/pattern.cjs +122 -0
- package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
- package/gsd-core/bin/lib/phase-id.cjs +444 -36
- package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
- package/gsd-core/bin/lib/phase-locator.cjs +125 -18
- package/gsd-core/bin/lib/phase.cjs +646 -143
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
- package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
- package/gsd-core/bin/lib/plan-scan.cjs +86 -2
- package/gsd-core/bin/lib/planning-scope.cjs +31 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
- package/gsd-core/bin/lib/probe-core.cjs +1 -1
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
- 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 +34 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
- package/gsd-core/bin/lib/roadmap.cjs +385 -94
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -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 +115 -3
- package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
- package/gsd-core/bin/lib/security.cjs +104 -5
- package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
- package/gsd-core/bin/lib/smart-entry.cjs +142 -22
- package/gsd-core/bin/lib/state-command-router.cjs +5 -1
- package/gsd-core/bin/lib/state-document.cjs +152 -8
- package/gsd-core/bin/lib/state-transition.cjs +371 -117
- package/gsd-core/bin/lib/state.cjs +1794 -357
- package/gsd-core/bin/lib/surface.cjs +23 -9
- 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 +9 -3
- package/gsd-core/bin/lib/uat.cjs +399 -56
- package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
- package/gsd-core/bin/lib/unusable-input.cjs +24 -0
- package/gsd-core/bin/lib/update-context.cjs +8 -2
- package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
- package/gsd-core/bin/lib/validate.cjs +20 -6
- package/gsd-core/bin/lib/vendor/README.md +37 -0
- package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
- package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +258 -8
- package/gsd-core/bin/lib/verify.cjs +368 -888
- 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.cjs +2 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
- package/gsd-core/references/agent-contracts.md +43 -26
- package/gsd-core/references/checkpoints.md +2 -2
- package/gsd-core/references/context-budget.md +1 -1
- package/gsd-core/references/dispatch-isolation-gate.md +138 -0
- package/gsd-core/references/doc-conflict-engine.md +1 -1
- package/gsd-core/references/execute-mvp-tdd.md +3 -3
- package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
- package/gsd-core/references/execute-phase-context-guard.md +1 -1
- package/gsd-core/references/execute-phase-response-language.md +1 -1
- package/gsd-core/references/execute-phase-wave-guard.md +6 -2
- package/gsd-core/references/gate-prompts.md +1 -1
- package/gsd-core/references/git-planning-commit.md +2 -1
- package/gsd-core/references/loop-hook-dispatch.md +39 -2
- package/gsd-core/references/model-profiles.md +12 -4
- package/gsd-core/references/mvp-concepts.md +9 -9
- package/gsd-core/references/planner-guidance.md +3 -9
- package/gsd-core/references/planner-preconditions.md +1 -1
- package/gsd-core/references/planner-reviews.md +1 -1
- package/gsd-core/references/planning-config.md +8 -6
- package/gsd-core/references/revision-loop.md +1 -1
- package/gsd-core/references/specless-probe-fallback.md +1 -1
- package/gsd-core/references/universal-anti-patterns.md +3 -3
- package/gsd-core/references/verifier-phase-gates.md +192 -0
- package/gsd-core/references/verify-mvp-mode.md +1 -1
- package/gsd-core/references/workstream-flag.md +22 -6
- package/gsd-core/templates/discussion-log.md +1 -1
- package/gsd-core/templates/phase-prompt.md +2 -4
- package/gsd-core/templates/state.md +4 -4
- package/gsd-core/templates/verification-report.md +9 -1
- package/gsd-core/workflows/ai-integration-phase.md +9 -11
- package/gsd-core/workflows/autonomous.md +1 -1
- package/gsd-core/workflows/cleanup.md +62 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
- package/gsd-core/workflows/code-review-fix.md +37 -10
- package/gsd-core/workflows/code-review.md +38 -12
- package/gsd-core/workflows/complete-milestone.md +141 -18
- package/gsd-core/workflows/debug.md +7 -5
- package/gsd-core/workflows/diagnose-issues.md +35 -9
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
- package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
- package/gsd-core/workflows/edit-phase.md +26 -1
- package/gsd-core/workflows/eval-review.md +3 -5
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
- 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 +2 -0
- package/gsd-core/workflows/execute-phase.md +38 -50
- package/gsd-core/workflows/execute-plan.md +36 -4
- package/gsd-core/workflows/explore.md +131 -4
- package/gsd-core/workflows/fast.md +10 -2
- package/gsd-core/workflows/health.md +73 -4
- package/gsd-core/workflows/import.md +4 -4
- package/gsd-core/workflows/ingest-docs.md +5 -5
- package/gsd-core/workflows/mvp-phase.md +6 -3
- package/gsd-core/workflows/new-milestone.md +14 -9
- package/gsd-core/workflows/new-project.md +14 -14
- package/gsd-core/workflows/next.md +12 -0
- package/gsd-core/workflows/plan-phase.md +41 -17
- package/gsd-core/workflows/plan-review-convergence.md +50 -2
- package/gsd-core/workflows/progress.md +34 -6
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
- package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
- package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
- package/gsd-core/workflows/quick.md +35 -15
- package/gsd-core/workflows/review.md +26 -5
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -1
- package/gsd-core/workflows/settings.md +66 -2
- package/gsd-core/workflows/ship.md +104 -44
- package/gsd-core/workflows/spec-phase.md +30 -12
- package/gsd-core/workflows/sync-skills.md +63 -8
- package/gsd-core/workflows/transition.md +46 -11
- package/gsd-core/workflows/ui-phase.md +5 -5
- package/gsd-core/workflows/ui-review.md +2 -2
- package/gsd-core/workflows/update.md +1 -1
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +9 -7
- package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
- package/hooks/dist/gsd-check-update-worker.js +56 -13
- package/hooks/dist/gsd-check-update.js +19 -1
- package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
- package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
- package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/dist/gsd-prompt-guard.js +21 -20
- package/hooks/dist/gsd-read-injection-scanner.js +38 -24
- package/hooks/dist/gsd-statusline.js +18 -0
- package/hooks/dist/gsd-update-banner.js +22 -1
- package/hooks/dist/gsd-workflow-guard.js +134 -36
- package/hooks/dist/lib/git-cmd.js +92 -59
- package/hooks/dist/lib/injection-patterns.js +45 -0
- package/hooks/dist/lib/isolation-deny-reason.js +39 -0
- package/hooks/dist/lib/isolation-sentinel.js +9 -0
- package/hooks/gsd-agent-isolation-guard.js +103 -14
- package/hooks/gsd-check-update-worker.js +56 -13
- package/hooks/gsd-check-update.js +19 -1
- package/hooks/gsd-cursor-pre-tool.js +0 -3
- package/hooks/gsd-cursor-subagent-start.js +77 -2
- package/hooks/gsd-cursor-subagent-stop.js +3 -2
- package/hooks/gsd-prompt-guard.js +21 -20
- package/hooks/gsd-read-injection-scanner.js +38 -24
- package/hooks/gsd-statusline.js +18 -0
- package/hooks/gsd-update-banner.js +22 -1
- package/hooks/gsd-workflow-guard.js +134 -36
- package/hooks/lib/git-cmd.js +92 -59
- package/hooks/lib/injection-patterns.js +45 -0
- package/hooks/lib/isolation-deny-reason.js +39 -0
- package/hooks/lib/isolation-sentinel.js +9 -0
- package/package.json +21 -9
- package/pi/gsd.cjs +19 -5
- 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/changeset/lint.cjs +60 -5
- package/scripts/check-alias-drift.cjs +7 -43
- package/scripts/check-contract-drift.cjs +297 -0
- package/scripts/ci-test-scope.cjs +19 -2
- package/scripts/command-contract-helpers.cjs +903 -1
- package/scripts/gen-adr-index.cjs +728 -38
- package/scripts/gen-capability-registry.cjs +3 -15
- package/scripts/gen-context-index.cjs +2 -11
- package/scripts/gen-health-docs.cjs +390 -0
- package/scripts/gen-inventory-manifest.cjs +50 -4
- package/scripts/gen-loop-host-contract.cjs +4 -24
- package/scripts/gen-registry.cjs +3 -14
- package/scripts/lib/alias-drift-families.cjs +46 -0
- package/scripts/lib/drift-scan.cjs +278 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +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-eslint-glob-coverage.allowlist.json +34 -0
- package/scripts/lint-eslint-glob-coverage.cjs +340 -0
- package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
- package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
- package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
- package/scripts/lint-milestone-window-drift.cjs +468 -0
- package/scripts/lint-phase-enumeration-drift.cjs +479 -0
- package/scripts/lint-plan-count-drift.cjs +318 -0
- package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
- package/scripts/lint-planning-prompt-drift.cjs +434 -0
- package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
- package/scripts/lint-regression-test-names.cjs +15 -13
- package/scripts/lint-removed-but-needed.cjs +320 -0
- package/scripts/lint-state-field-drift.cjs +805 -0
- package/scripts/lint-state-write-path-drift.cjs +1045 -0
- package/scripts/lint-test-file-count.allowlist.json +21 -10
- package/scripts/lint-unreachable-guard-drift.cjs +843 -0
- package/scripts/lint-vendored-deps.cjs +124 -0
- package/scripts/pr-changed-files.cjs +63 -0
- package/scripts/pr-template-policy.cjs +14 -4
- package/scripts/prompt-injection-scan.sh +25 -0
- package/scripts/require-issue-link-policy.cjs +192 -0
- package/scripts/state-write-path-drift-baseline.json +19 -0
- package/scripts/sync-runtime-launcher.cjs +2 -4
- package/skills/gsd-autonomous/SKILL.md +0 -1
- package/skills/gsd-code-review/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -2
- package/skills/gsd-map-codebase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +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 +1 -1
- package/skills/gsd-review-backlog/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +0 -1
- package/skills/gsd-verify-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
- package/gsd-core/workflows/discovery-phase.md +0 -298
- package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
- package/gsd-core/workflows/verify-phase.md +0 -574
- package/scripts/affected-tests-lib.cjs +0 -554
- package/scripts/lint-allow-test-rule-refs.cjs +0 -162
- package/scripts/run-affected-tests.cjs +0 -7
- package/scripts/run-tests.cjs +0 -1051
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Anti-divergence drift guard for the live-plan-counting seam
|
|
6
|
+
* (epic #3180, issue #3183, ADR-3180 "Planning Semantic Model Single Owner").
|
|
7
|
+
*
|
|
8
|
+
* `src/plan-scan.cts`'s `scanPhasePlans` is the SINGLE canonical owner of
|
|
9
|
+
* live-plan/summary counting: which files on disk are a "plan", which are a
|
|
10
|
+
* "summary", and how the two pair up. Every other module that reads a phase
|
|
11
|
+
* directory and re-derives that filename grammar itself — `readdirSync(...)`
|
|
12
|
+
* filtered by an inline `-PLAN.md` / `PLAN.md` / `-SUMMARY.md` / `SUMMARY.md`
|
|
13
|
+
* pattern — is a re-derivation that can silently drift from the owner (the
|
|
14
|
+
* exact failure class this epic removes; see #2349, #1988).
|
|
15
|
+
*
|
|
16
|
+
* Per ADR-3180 Decision 4(a) this guard discovers call sites by SCANNING THE
|
|
17
|
+
* WHOLE `src/` TREE, not by consulting an allowlist of known files — an
|
|
18
|
+
* allowlist only measures re-derivations in files someone remembered to
|
|
19
|
+
* list, and a new call site added anywhere else would sail through silently.
|
|
20
|
+
*
|
|
21
|
+
* Detection is intentionally NARROW and mirrors the existing
|
|
22
|
+
* `lint-phase-id-drift.cjs` precedent: a small, readable per-line regex pair
|
|
23
|
+
* over authored TypeScript source, with a short, explicitly-named exemption
|
|
24
|
+
* list — not a general-purpose AST/control-flow analysis. A line counts as a
|
|
25
|
+
* re-derivation when it contains BOTH:
|
|
26
|
+
* (a) a filename-TEST operation — `.filter(`, `.test(`, `.match(`,
|
|
27
|
+
* `.exec(`, `.endsWith(`, `.startsWith(`, `.includes(`, `.some(`,
|
|
28
|
+
* `.every(`, or `===` — and
|
|
29
|
+
* (b) a plan/summary filename-suffix pattern, either a quoted literal
|
|
30
|
+
* ('-PLAN.md', 'PLAN.md', '-SUMMARY.md', 'SUMMARY.md') OR an unquoted
|
|
31
|
+
* regex literal that mentions PLAN or SUMMARY and `\.md` together
|
|
32
|
+
* (`/-PLAN\.md$/`, `/^PLAN-\d+.*\.md$/i`)
|
|
33
|
+
* on the same source line. #3183 originally required (a) to be specifically
|
|
34
|
+
* `.filter(` on the SAME line as the literal — that missed a regex-literal
|
|
35
|
+
* predicate (`files.filter(f => /-PLAN\.md$/.test(f))`, no quotes) and a
|
|
36
|
+
* predicate defined on one line and consumed by `.filter(` on another
|
|
37
|
+
* (`const isPlan = f => f.endsWith('-PLAN.md'); … files.filter(isPlan)`).
|
|
38
|
+
* Widening (a) to any filename-test operator — not just `.filter(` itself —
|
|
39
|
+
* catches both: the predicate's OWN line already carries a qualifying test
|
|
40
|
+
* operation (`.test(`/`.endsWith(`) alongside the literal, independent of
|
|
41
|
+
* where `.filter(` ends up.
|
|
42
|
+
*
|
|
43
|
+
* KNOWN, ACCEPTED limits of a per-line textual scan (same tradeoff the
|
|
44
|
+
* phase-id-drift guard documents): a re-derivation that filters via a
|
|
45
|
+
* hand-rolled loop with none of the listed test operators (e.g. a manual
|
|
46
|
+
* character-index scan), or one whose literal and test operator are split
|
|
47
|
+
* across two DIFFERENT lines with no single line carrying both, is not
|
|
48
|
+
* caught by this narrow shape. That is left to code review, not this regex.
|
|
49
|
+
*
|
|
50
|
+
* The tree-walk / root-confinement / regex-literal-tokenizer / sanitizer
|
|
51
|
+
* machinery below is SHARED with `scripts/lint-milestone-window-drift.cjs`
|
|
52
|
+
* (#3184) via `scripts/lib/drift-scan.cjs` — see that module for the
|
|
53
|
+
* `isInsideRoot` case-sensitivity note, the `walk` symlink-confinement
|
|
54
|
+
* rationale, and the `readRegexLiteralAt` tokenizer's ReDoS-avoidance
|
|
55
|
+
* rationale. It is deliberately NOT duplicated here a second time (ADR-3180
|
|
56
|
+
* Decision 4's own "Rejected" list: "let the new drift guard copy Phase 1's
|
|
57
|
+
* tree-walk / root-confinement / sanitizer").
|
|
58
|
+
*
|
|
59
|
+
* A regex literal longer than MAX_REGEX_LITERAL_LEN (400) characters is not
|
|
60
|
+
* read, and is therefore not caught. That bound is what keeps the scan
|
|
61
|
+
* linear; no real plan/summary filename filter approaches it. The scan is
|
|
62
|
+
* scoped to SCAN_DIRS (`src`) with SCAN_EXT (.cts/.ts/.mts) — 186 files and
|
|
63
|
+
* 4,299 lines matching FILENAME_TEST_RE within SCAN_DIRS/SCAN_EXT as of this
|
|
64
|
+
* commit, 43 of them holding 7 or more backslashes and one (`src/milestone.cts`)
|
|
65
|
+
* holding 16. (Definition used, so this is reproducible: walk SCAN_DIRS
|
|
66
|
+
* filtering by SCAN_EXT exactly as `walk` does, split each file on `\n`, and
|
|
67
|
+
* count every line for which the exported `FILENAME_TEST_RE.test(line)` is
|
|
68
|
+
* true — independent of whether a PLAN/SUMMARY literal is also present on
|
|
69
|
+
* that line.) Those are the lines the old backtracking detector had to
|
|
70
|
+
* survive, and the reason the detector is now a tokenizer.
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
const path = require('node:path');
|
|
74
|
+
const driftScan = require('./lib/drift-scan.cjs');
|
|
75
|
+
const { readRegexLiteralAt, MAX_REGEX_LITERAL_LEN, isInsideRoot, sanitizeForReport, scanTree } = driftScan;
|
|
76
|
+
|
|
77
|
+
// A `.filter(` call on the line — the shape every current re-derivation uses
|
|
78
|
+
// to turn a directory listing into a plan-or-summary subset. Kept as its own
|
|
79
|
+
// export for back-compat / documentation; FILENAME_TEST_RE below is the
|
|
80
|
+
// broadened detector actually used (any filename-test operator, not just
|
|
81
|
+
// `.filter(`).
|
|
82
|
+
const FILTER_CALL_RE = /\.filter\(/;
|
|
83
|
+
|
|
84
|
+
// A filename-TEST operation: `.filter(`, `.test(`, `.match(`, `.exec(`,
|
|
85
|
+
// `.endsWith(`, `.startsWith(`, `.includes(`, `.some(`, `.every(`, or a
|
|
86
|
+
// strict-equality comparison. Any one of these on a line asking "is this
|
|
87
|
+
// filename a plan/summary" is a re-derivation, independent of whether the
|
|
88
|
+
// literal shows up as a `.filter(...)` predicate specifically.
|
|
89
|
+
const FILENAME_TEST_RE = /\.(?:filter|test|match|exec|endsWith|startsWith|includes|some|every)\(|===/;
|
|
90
|
+
|
|
91
|
+
// A quoted plan/summary filename-suffix literal: 'PLAN.md', '-PLAN.md',
|
|
92
|
+
// 'SUMMARY.md', or '-SUMMARY.md', single- or double-quoted (opening and
|
|
93
|
+
// closing quote must match).
|
|
94
|
+
const PLAN_SUMMARY_LITERAL_RE = /(['"])-?(?:PLAN|SUMMARY)\.md\1/;
|
|
95
|
+
|
|
96
|
+
// The two tokens that, appearing together INSIDE one regex literal, make it a
|
|
97
|
+
// plan/summary filename filter. `\.md` is matched as literal source text, not
|
|
98
|
+
// as a pattern, so there is nothing here to backtrack.
|
|
99
|
+
const PLAN_SUMMARY_TOKEN_RE = /PLAN|SUMMARY/i;
|
|
100
|
+
const ESCAPED_MD_TOKEN = '\\.md';
|
|
101
|
+
|
|
102
|
+
// Authored TypeScript source only (the generated bin/lib/*.cjs mirror it).
|
|
103
|
+
const SCAN_DIRS = ['src'];
|
|
104
|
+
const SCAN_EXT = new Set(['.cts', '.ts', '.mts']);
|
|
105
|
+
|
|
106
|
+
// The canonical owner defines the grammar; it is exempt by construction.
|
|
107
|
+
const OWNER_FILE = path.join('src', 'plan-scan.cts');
|
|
108
|
+
|
|
109
|
+
// core-utils.cts's canonical pairing rule (#1988/#2648): these three
|
|
110
|
+
// functions build/match `*-SUMMARY.md` CANDIDATE strings for a given plan —
|
|
111
|
+
// that IS the single pairing rule, not a re-derivation of it. Scoped to just
|
|
112
|
+
// these functions (not the whole file) so an unrelated re-derivation added
|
|
113
|
+
// elsewhere in core-utils.cts is still caught.
|
|
114
|
+
const CORE_UTILS_FILE = path.join('src', 'core-utils.cts');
|
|
115
|
+
const CORE_UTILS_EXEMPT_FUNCTIONS = new Set([
|
|
116
|
+
'summaryCandidates',
|
|
117
|
+
'countMatchedSummaries',
|
|
118
|
+
'findUnsummarizedPlans',
|
|
119
|
+
'findOrphanSummaries',
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
// Per ADR-3180 Decision 4(a): NOT a bare file allowlist — each entry below is
|
|
123
|
+
// scoped to the SPECIFIC function asking a documented, different question
|
|
124
|
+
// (see the inline comment at each site), so an unrelated re-derivation added
|
|
125
|
+
// anywhere else in these same files is still caught. Mirrors the
|
|
126
|
+
// CORE_UTILS_EXEMPT_FUNCTIONS mechanism above, generalized per-file.
|
|
127
|
+
//
|
|
128
|
+
// - audit.cts resolveQuickTaskSummaryFile: scans a quick task's OWN
|
|
129
|
+
// directory (`.planning/quick/<task>/`) for that ONE task's completion
|
|
130
|
+
// record — not a phase directory's live-plan/summary counting question.
|
|
131
|
+
// #3458 follow-up extracted this out of `scanQuickTasks` (the prior
|
|
132
|
+
// exemption target) into its own function so `scanQuickTasks` (read) and
|
|
133
|
+
// `cmdAuditAcknowledge`'s quick_tasks writer share the ONE discovery
|
|
134
|
+
// rule instead of each re-deriving it independently.
|
|
135
|
+
// - gsd2-import.cts readTasksDir: reads a FOREIGN GSD-2 legacy project's
|
|
136
|
+
// `tasks/` dir convention during a one-time import, not this project's
|
|
137
|
+
// `.planning/phases/` layout at all.
|
|
138
|
+
// - estimate-cli.cts collectCalibrationSamples: pairs a PLAN.md and a
|
|
139
|
+
// SUMMARY.md by their identical `<stem>` to build an estimation
|
|
140
|
+
// CALIBRATION sample (projected vs. actual token counts) — a stem-keyed
|
|
141
|
+
// join for a statistics question, not a live-plan/completion count.
|
|
142
|
+
// It intentionally does NOT use the canonical three-candidate pairing
|
|
143
|
+
// rule (marker-swap / `-SUMMARY.md` / extended) or exclude superseded
|
|
144
|
+
// plans — an unmatched or superseded plan simply yields no sample,
|
|
145
|
+
// which is correct for calibration, not a live-completion determination.
|
|
146
|
+
// - roadmap.cts cmdRoadmapAnnotateDependencies: matches a plan-ID token
|
|
147
|
+
// out of an ALREADY-RENDERED ROADMAP.md checklist LINE OF TEXT
|
|
148
|
+
// (`- [ ] 01-01-PLAN.md — …`), not a filesystem directory listing — it
|
|
149
|
+
// can never diverge from scanPhasePlans's file-existence rule because it
|
|
150
|
+
// never tests file existence at all.
|
|
151
|
+
// - worktree-safety.cts defaultFindSummaryFiles: a recursive walk of the
|
|
152
|
+
// ENTIRE `.planning/` tree (not a single phase directory) for a
|
|
153
|
+
// pre-merge rescue of any `*SUMMARY.md` artifact, deliberately mirroring
|
|
154
|
+
// the shell fallback's own `find … -name "*SUMMARY.md"` glob (quick.md,
|
|
155
|
+
// #2296/#2070/#2838) byte-for-behaviour rather than the phase-scoped
|
|
156
|
+
// plan-scan owner's root+nested rule — "rescue every summary before a
|
|
157
|
+
// merge blows it away" is not a live-plan/completion count.
|
|
158
|
+
// - planning-snapshot.cts buildPerPhasePlanScanFields (Phase 12, #3310,
|
|
159
|
+
// ADR-3180 §8.4): a strict `-(\d{2})-PLAN\.md$` match extracts a
|
|
160
|
+
// zero-padded SEQUENCE NUMBER from filenames the owner (`allPlanFiles`)
|
|
161
|
+
// already classified as plans, into the `perPhasePlanNumbering`
|
|
162
|
+
// `PlanningSnapshot` field so `validate.consistency`'s C002 rule can read
|
|
163
|
+
// it via the shared snapshot instead of re-scanning disk. It does not
|
|
164
|
+
// re-derive "is this a plan" — it answers a different, narrower question
|
|
165
|
+
// (does the canonical 2-digit numbering sequence have a gap) that the
|
|
166
|
+
// owner's boolean plan/summary classification cannot answer.
|
|
167
|
+
const FUNCTION_SCOPED_EXEMPTIONS = new Map([
|
|
168
|
+
[CORE_UTILS_FILE, CORE_UTILS_EXEMPT_FUNCTIONS],
|
|
169
|
+
[path.join('src', 'audit.cts'), new Set(['resolveQuickTaskSummaryFile'])],
|
|
170
|
+
[path.join('src', 'gsd2-import.cts'), new Set(['readTasksDir'])],
|
|
171
|
+
[path.join('src', 'estimate-cli.cts'), new Set(['collectCalibrationSamples'])],
|
|
172
|
+
[path.join('src', 'roadmap.cts'), new Set(['cmdRoadmapAnnotateDependencies'])],
|
|
173
|
+
[path.join('src', 'worktree-safety.cts'), new Set(['defaultFindSummaryFiles'])],
|
|
174
|
+
[path.join('src', 'planning-snapshot.cts'), new Set(['buildPerPhasePlanScanFields'])],
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
// Optional `export ` modifier: `collectCalibrationSamples` (estimate-cli.cts)
|
|
178
|
+
// is declared `export function …` rather than a bare `function …`, and the
|
|
179
|
+
// function-boundary tracker below must still recognize it for its
|
|
180
|
+
// FUNCTION_SCOPED_EXEMPTIONS entry above to take effect.
|
|
181
|
+
const TOP_LEVEL_FUNCTION_RE = /^(?:export\s+)?function\s+([A-Za-z0-9_]+)\s*\(/;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The regex literal on `line` that mentions PLAN or SUMMARY together with an
|
|
185
|
+
* escaped `.md` suffix — e.g. `/-PLAN\.md$/`, `/^PLAN-\d+.*\.md$/i`,
|
|
186
|
+
* `/-SUMMARY-\d+.*\.md$/i` — or null if there is none. Replaces the former
|
|
187
|
+
* `REGEX_LITERAL_MD_RE`, which was both exponentially/cubically backtracking
|
|
188
|
+
* (CodeQL js/redos; this guard runs in `lint:ci` on fork PRs) and unable to
|
|
189
|
+
* see a `[\\/]` character class.
|
|
190
|
+
*/
|
|
191
|
+
function findRegexLiteralMdMatch(line) {
|
|
192
|
+
for (let i = 0; i < line.length; i++) {
|
|
193
|
+
if (line[i] !== '/') continue;
|
|
194
|
+
const literal = readRegexLiteralAt(line, i);
|
|
195
|
+
if (!literal) continue;
|
|
196
|
+
// Case-insensitive `\.md` test — the regex literal this replaced carried
|
|
197
|
+
// the `i` flag, so `\.MD`/`\.Md` must still match. A lowercased-copy
|
|
198
|
+
// `.includes()` preserves that behaviour without reintroducing a
|
|
199
|
+
// backtracking regex.
|
|
200
|
+
if (PLAN_SUMMARY_TOKEN_RE.test(literal.text) && literal.text.toLowerCase().includes(ESCAPED_MD_TOKEN)) {
|
|
201
|
+
return literal.text;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Strip comment text from a line before detection. A guard that fires on a
|
|
209
|
+
* COMMENT — including a comment documenting that the code below uses the
|
|
210
|
+
* canonical owner, or prose quoting this guard's own detector shapes — reports
|
|
211
|
+
* prose as drift and trains readers to add exemptions for documentation.
|
|
212
|
+
* Handles the three shapes that appear in this codebase: a whole-line
|
|
213
|
+
* block-comment continuation (`*` or `/*` leading), a `//` line comment, and
|
|
214
|
+
* a trailing `//` after code. Mirrors `lint-phase-enumeration-drift.cjs`'s
|
|
215
|
+
* own copy (not shared — each guard applies it at a slightly different point
|
|
216
|
+
* in its detection pipeline).
|
|
217
|
+
*
|
|
218
|
+
* Deliberately simple and conservative: it does not attempt full block-comment
|
|
219
|
+
* state tracking across lines (this is a per-line scan, same tradeoff the
|
|
220
|
+
* sibling guards document). A `//` inside a string literal would be stripped
|
|
221
|
+
* early — accepted, because the effect is to UNDER-report on a pathological
|
|
222
|
+
* line, never to over-report prose as drift.
|
|
223
|
+
*/
|
|
224
|
+
function stripComments(line) {
|
|
225
|
+
const trimmed = line.trim();
|
|
226
|
+
// Whole-line block comment or JSDoc continuation.
|
|
227
|
+
if (trimmed.startsWith('*') || trimmed.startsWith('/*') || trimmed.startsWith('//')) return '';
|
|
228
|
+
// Trailing line comment after code.
|
|
229
|
+
const idx = line.indexOf('//');
|
|
230
|
+
return idx === -1 ? line : line.slice(0, idx);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Pure: find every unsanctioned plan/summary-filter re-derivation in `text`.
|
|
235
|
+
* `relPath` is the repo-relative path, used both to report file:line and to
|
|
236
|
+
* apply the narrow, function-scoped core-utils.cts exemption.
|
|
237
|
+
* Returns [{ line, found }].
|
|
238
|
+
*/
|
|
239
|
+
function findPlanCountDrift(text, relPath) {
|
|
240
|
+
const out = [];
|
|
241
|
+
const lines = text.split('\n');
|
|
242
|
+
const exemptFunctions = FUNCTION_SCOPED_EXEMPTIONS.get(relPath) || null;
|
|
243
|
+
let currentFunction = null;
|
|
244
|
+
for (let i = 0; i < lines.length; i++) {
|
|
245
|
+
const line = lines[i];
|
|
246
|
+
const fnMatch = TOP_LEVEL_FUNCTION_RE.exec(line);
|
|
247
|
+
if (fnMatch) currentFunction = fnMatch[1];
|
|
248
|
+
|
|
249
|
+
const code = stripComments(line);
|
|
250
|
+
if (!code.trim()) continue;
|
|
251
|
+
|
|
252
|
+
if (!FILENAME_TEST_RE.test(code)) continue;
|
|
253
|
+
const quoted = PLAN_SUMMARY_LITERAL_RE.exec(code);
|
|
254
|
+
const found = quoted ? quoted[0] : findRegexLiteralMdMatch(code);
|
|
255
|
+
if (!found) continue;
|
|
256
|
+
|
|
257
|
+
if (exemptFunctions && exemptFunctions.has(currentFunction)) continue;
|
|
258
|
+
|
|
259
|
+
out.push({ line: i + 1, found });
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Scan the authored source tree and return every unsanctioned re-derivation,
|
|
266
|
+
* each annotated with the repo-relative file path.
|
|
267
|
+
*/
|
|
268
|
+
function scanRepo(root) {
|
|
269
|
+
return scanTree({
|
|
270
|
+
root,
|
|
271
|
+
scanDirs: SCAN_DIRS,
|
|
272
|
+
scanExt: SCAN_EXT,
|
|
273
|
+
onFile(rel, text) {
|
|
274
|
+
// `rel` is already the REAL (canonical) path (scanTree resolves
|
|
275
|
+
// symlinks before calling onFile), so this comparison — and
|
|
276
|
+
// FUNCTION_SCOPED_EXEMPTIONS above, also keyed on `rel` — match
|
|
277
|
+
// consistently regardless of which symlink reached the file.
|
|
278
|
+
if (rel === OWNER_FILE) return [];
|
|
279
|
+
return findPlanCountDrift(text, rel).map((d) => ({ file: rel, ...d }));
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function main() {
|
|
285
|
+
const root = path.join(__dirname, '..');
|
|
286
|
+
const violations = scanRepo(root);
|
|
287
|
+
if (violations.length === 0) {
|
|
288
|
+
process.stdout.write('ok plan-count-drift: no unsanctioned plan/summary re-derivations outside plan-scan.cts\n');
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
process.stderr.write('plan-count-drift: independent re-derivation(s) of plan/summary filename filtering found.\n');
|
|
292
|
+
process.stderr.write('Use src/plan-scan.cjs `scanPhasePlans` (or core-utils.cjs `getPhaseFileStats`, which now\n');
|
|
293
|
+
process.stderr.write('sources plans/summaries from it) instead of re-deriving the -PLAN.md/-SUMMARY.md filter:\n');
|
|
294
|
+
for (const d of violations) {
|
|
295
|
+
// `d.file` is exactly as attacker-controlled as `d.found`: a repo can
|
|
296
|
+
// legally track a filename containing control bytes / bidi overrides,
|
|
297
|
+
// and it is a fork-PR-authored value reaching a CI log the same way the
|
|
298
|
+
// matched literal does — sanitize it at the same reporting boundary.
|
|
299
|
+
process.stderr.write(` ${sanitizeForReport(d.file)}:${d.line} ${sanitizeForReport(d.found)}\n`);
|
|
300
|
+
}
|
|
301
|
+
process.exitCode = 1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (require.main === module) main();
|
|
305
|
+
|
|
306
|
+
module.exports = {
|
|
307
|
+
findPlanCountDrift,
|
|
308
|
+
scanRepo,
|
|
309
|
+
FILTER_CALL_RE,
|
|
310
|
+
FILENAME_TEST_RE,
|
|
311
|
+
PLAN_SUMMARY_LITERAL_RE,
|
|
312
|
+
findRegexLiteralMdMatch,
|
|
313
|
+
readRegexLiteralAt,
|
|
314
|
+
MAX_REGEX_LITERAL_LEN,
|
|
315
|
+
isInsideRoot,
|
|
316
|
+
sanitizeForReport,
|
|
317
|
+
stripComments,
|
|
318
|
+
};
|