@opengsd/gsd-core 1.14.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-code-fixer.compact.md +7 -6
- package/agents/gsd-code-fixer.md +9 -8
- package/agents/gsd-debug-session-manager.compact.md +17 -2
- package/agents/gsd-debug-session-manager.md +17 -2
- package/agents/gsd-debugger.md +2 -2
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +13 -8
- package/agents/gsd-intel-updater.compact.md +1 -1
- package/agents/gsd-intel-updater.md +1 -1
- 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 +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-ui-auditor.md +155 -17
- package/agents/gsd-ui-researcher.compact.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +10 -9
- package/bin/install.js +642 -95
- package/commands/gsd/autonomous.md +2 -2
- package/commands/gsd/capture.md +1 -1
- package/commands/gsd/mempalace-capture.md +7 -3
- package/commands/gsd/plan-review-convergence.md +6 -6
- package/commands/gsd/progress.md +1 -1
- package/commands/gsd/quick-batch.md +1 -1
- package/commands/gsd/review.md +2 -3
- package/gsd-core/bin/gsd-tools.cjs +335 -22
- package/gsd-core/bin/lib/adr-parser.cjs +3 -1
- package/gsd-core/bin/lib/audit.cjs +81 -13
- package/gsd-core/bin/lib/capability-registry.cjs +82 -187
- package/gsd-core/bin/lib/capability-validator.cjs +0 -1
- package/gsd-core/bin/lib/check-command-router.cjs +101 -14
- package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
- package/gsd-core/bin/lib/commands.cjs +175 -42
- package/gsd-core/bin/lib/config-loader.cjs +65 -4
- package/gsd-core/bin/lib/config.cjs +33 -7
- package/gsd-core/bin/lib/decisions.cjs +30 -14
- package/gsd-core/bin/lib/frontmatter.cjs +13 -0
- package/gsd-core/bin/lib/graphify.cjs +10 -2
- package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
- package/gsd-core/bin/lib/init.cjs +207 -41
- package/gsd-core/bin/lib/install-engine.cjs +13 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +8 -1
- package/gsd-core/bin/lib/milestone.cjs +18 -5
- package/gsd-core/bin/lib/model-resolver.cjs +159 -50
- package/gsd-core/bin/lib/phase-command-router.cjs +9 -1
- 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 +109 -7
- package/gsd-core/bin/lib/phase-locator.cjs +29 -10
- package/gsd-core/bin/lib/phase.cjs +227 -26
- 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 +18 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +8 -3
- package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
- package/gsd-core/bin/lib/probe-core.cjs +7 -1
- package/gsd-core/bin/lib/project-root.cjs +41 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
- package/gsd-core/bin/lib/roadmap-parser.cjs +163 -3
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
- package/gsd-core/bin/lib/roadmap.cjs +251 -31
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +283 -31
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -33
- package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
- package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
- package/gsd-core/bin/lib/state-transition.cjs +39 -2
- package/gsd-core/bin/lib/state.cjs +42 -0
- package/gsd-core/bin/lib/surface.cjs +17 -1
- 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 +8 -0
- 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/verification.cjs +268 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +46 -2
- package/gsd-core/bin/lib/verify.cjs +132 -25
- package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
- package/gsd-core/bin/lib/worktree-safety.cjs +784 -51
- package/gsd-core/bin/shared/config-defaults.manifest.json +3 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/checkpoints.md +5 -3
- 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/model-profiles.md +1 -1
- 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 +11 -2
- package/gsd-core/references/tdd.md +27 -4
- package/gsd-core/references/ui-consideration-probe.md +10 -5
- package/gsd-core/references/verify-command-path-resolvability.md +10 -2
- package/gsd-core/references/worktree-path-safety.md +321 -0
- 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 +3 -3
- 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 +16 -6
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/cleanup.md +2 -2
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +108 -22
- package/gsd-core/workflows/code-review.md +63 -46
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +1 -1
- package/gsd-core/workflows/complete-milestone.md +2 -2
- 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.md +3 -3
- 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 +2 -2
- package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +3 -3
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +37 -3
- 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/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 +36 -26
- package/gsd-core/workflows/execute-plan.md +5 -4
- 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 +3 -3
- package/gsd-core/workflows/help/modes/full.md +5 -5
- package/gsd-core/workflows/help/modes/topic.md +15 -5
- 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 +2 -2
- 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 +2 -2
- 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 +7 -7
- 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 +1 -1
- 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 +24 -7
- 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 +113 -13
- 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 +21 -9
- package/gsd-core/workflows/reapply-patches.md +9 -3
- 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 +4 -4
- 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 +7 -7
- package/gsd-core/workflows/validate-phase.md +3 -2
- 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 +81 -16
- package/hooks/dist/gsd-agent-isolation-guard.js +24 -0
- package/hooks/dist/gsd-secret-read-guard.js +27 -1
- package/hooks/dist/gsd-statusline.js +70 -13
- package/hooks/dist/gsd-validate-commit.sh +63 -4
- package/hooks/gsd-agent-isolation-guard.js +24 -0
- package/hooks/gsd-secret-read-guard.js +27 -1
- package/hooks/gsd-statusline.js +70 -13
- package/hooks/gsd-validate-commit.sh +63 -4
- package/package.json +3 -2
- package/scripts/build-hooks.js +15 -6
- package/scripts/check-contract-drift.cjs +127 -11
- package/scripts/command-contract-helpers.cjs +3 -0
- package/scripts/docs-guard-registry.cjs +28 -0
- package/scripts/gen-loop-host-contract.cjs +69 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +14 -0
- package/scripts/lib/ndjson-reporter.cjs +3 -2
- package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +28 -1
- package/scripts/lint-phase-arg-assignment.cjs +257 -0
- package/scripts/lint-phase-id-drift.cjs +290 -5
- package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
- package/scripts/lint-retired-runtime-name.cjs +619 -0
- package/scripts/lint-state-write-path-drift.cjs +93 -0
- package/scripts/lint-test-file-count.allowlist.json +28 -9
- package/scripts/lint-workflow-shellcheck-baseline.json +15 -0
- package/scripts/prompt-injection-scan.sh +4 -0
- package/scripts/release-tarball-smoke.cjs +194 -1
- package/skills/gsd-autonomous/SKILL.md +2 -2
- package/skills/gsd-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +7 -3
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
- package/skills/gsd-progress/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +1 -1
- package/skills/gsd-review/SKILL.md +2 -3
- package/vscode/package.json +1 -1
|
@@ -37,6 +37,22 @@
|
|
|
37
37
|
* over markdown, not `.cts` source, so its sanction is an HTML comment on the
|
|
38
38
|
* nearest preceding non-blank line: `<!-- phase-id-owner: <reason> -->`.
|
|
39
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
|
+
*
|
|
40
56
|
* - branch-slug fallback drift: a `.replace('{slug}', ... || 'phase')` call
|
|
41
57
|
* silently substitutes the literal string `'phase'` when a phase's slug
|
|
42
58
|
* can't be derived, producing a non-identifying branch name like
|
|
@@ -234,6 +250,39 @@ function findBranchSlugFallbackDrift(text) {
|
|
|
234
250
|
return out;
|
|
235
251
|
}
|
|
236
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
|
+
|
|
237
286
|
// #4634: ban base-10-forced shell arithmetic (`$((10#...))`) on a variable
|
|
238
287
|
// that still carries a possibly-decimal/multi-segment phase id — this
|
|
239
288
|
// construct is exactly the pattern that breaks on a value like `08.5`. The
|
|
@@ -246,11 +295,14 @@ function findBranchSlugFallbackDrift(text) {
|
|
|
246
295
|
// 1. Prose mentioning the literal pattern in a full-line `#`-comment
|
|
247
296
|
// (filtered by the caller, not this regex — see below).
|
|
248
297
|
// 2. `$((10#$PHASE_INT))` / `$((10#$SPOT_PHASE_INT))` — arithmetic on the
|
|
249
|
-
// NOW-safe variable the #4619 fix produces via
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
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.
|
|
254
306
|
// 3. `$((10#{plan_padded}))` / `$((10#${PLAN_ID}))` — plan ids are plain
|
|
255
307
|
// integers and were never in scope; this rule only polices variables
|
|
256
308
|
// that carry a *phase* id.
|
|
@@ -389,6 +441,204 @@ function scanMarkdownSingleSegmentPhaseRegex(root) {
|
|
|
389
441
|
return violations;
|
|
390
442
|
}
|
|
391
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;
|
|
575
|
+
out.push({ line: i + 1, found: m[0] });
|
|
576
|
+
}
|
|
577
|
+
return out;
|
|
578
|
+
}
|
|
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
|
+
|
|
392
642
|
// Authored TypeScript source only (the generated bin/lib/*.cjs mirror it).
|
|
393
643
|
const SCAN_DIRS = ['src'];
|
|
394
644
|
const SCAN_EXT = new Set(['.cts', '.ts', '.mts']);
|
|
@@ -523,6 +773,16 @@ function scanRepo(root) {
|
|
|
523
773
|
for (const d of findBranchSlugFallbackDrift(text)) {
|
|
524
774
|
violations.push({ file: rel, kind: 'branch-slug-fallback', ...d });
|
|
525
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
|
+
}
|
|
526
786
|
}
|
|
527
787
|
}
|
|
528
788
|
return violations;
|
|
@@ -541,6 +801,8 @@ function scanAll(root) {
|
|
|
541
801
|
...scanRepo(root),
|
|
542
802
|
...scanMarkdownShellArith(root),
|
|
543
803
|
...scanMarkdownSingleSegmentPhaseRegex(root),
|
|
804
|
+
...scanMarkdownLetterlessPhaseMirror(root),
|
|
805
|
+
...scanMarkdownLetterAxisConsumers(root),
|
|
544
806
|
];
|
|
545
807
|
}
|
|
546
808
|
|
|
@@ -567,8 +829,19 @@ function main() {
|
|
|
567
829
|
process.stderr.write('near-variant) is banned outright in gsd-core/workflows/**/*.md,\n');
|
|
568
830
|
process.stderr.write('gsd-core/references/**/*.md, and agents/**/*.md — widen it to `*` (unbounded\n');
|
|
569
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');
|
|
570
840
|
process.stderr.write('A `.replace(\'{slug}\', ... || \'phase\')` fallback is banned outright (#4126) —\n');
|
|
571
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');
|
|
572
845
|
process.stderr.write('`// phase-id-owner: <reason>` on the line directly above:\n');
|
|
573
846
|
for (const d of violations) {
|
|
574
847
|
process.stderr.write(` [${d.kind}] ${d.file}:${d.line} ${d.found}\n`);
|
|
@@ -583,10 +856,17 @@ module.exports = {
|
|
|
583
856
|
findBracketGrammarDrift,
|
|
584
857
|
findNameValidityDrift,
|
|
585
858
|
findBranchSlugFallbackDrift,
|
|
859
|
+
findPhaseHeadingScanLiteralDrift,
|
|
586
860
|
findShellPhaseArithDrift,
|
|
587
861
|
findSingleSegmentPhaseRegexDrift,
|
|
862
|
+
findLetterlessPhaseMirrorDrift,
|
|
863
|
+
findDotOnlyIntegerSplitDrift,
|
|
864
|
+
findLooseDottedPhaseRegexDrift,
|
|
865
|
+
findShellPhasePrintfPadDrift,
|
|
588
866
|
scanMarkdownShellArith,
|
|
589
867
|
scanMarkdownSingleSegmentPhaseRegex,
|
|
868
|
+
scanMarkdownLetterlessPhaseMirror,
|
|
869
|
+
scanMarkdownLetterAxisConsumers,
|
|
590
870
|
scanRepo,
|
|
591
871
|
scanAll,
|
|
592
872
|
countSelectorBaselines,
|
|
@@ -595,6 +875,11 @@ module.exports = {
|
|
|
595
875
|
BRACKET_CODE_DRIFT_RE,
|
|
596
876
|
NAME_VALIDITY_DRIFT_RE,
|
|
597
877
|
BRANCH_SLUG_FALLBACK_DRIFT_RE,
|
|
878
|
+
PHASE_HEADING_SCAN_LITERAL_DRIFT_RE,
|
|
598
879
|
SHELL_PHASE_ARITH_DRIFT_RE,
|
|
599
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,
|
|
600
885
|
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PR-branch `.planning/` path-classification drift guard (ADR-4910 §8, epic
|
|
6
|
+
* #4906 Phase 5; issues #4605, #4606).
|
|
7
|
+
*
|
|
8
|
+
* `src/pr-branch-patterns.cts` (compiled to
|
|
9
|
+
* `gsd-core/bin/lib/pr-branch-patterns.cjs`) is the single-source-of-truth
|
|
10
|
+
* registry for the three `.planning/` path-classification values
|
|
11
|
+
* `gsd-core/workflows/pr-branch.md` needs: `TRANSIENT_DIRS_SRC`,
|
|
12
|
+
* `STRUCTURAL_RE_SRC`, and `MILESTONE_PHASES_RE_SRC`. A workflow markdown
|
|
13
|
+
* file has no module system — it cannot `require()` that `.cts` source at
|
|
14
|
+
* runtime — so its `create_pr_branch`/`verify` steps carry a hand-copied
|
|
15
|
+
* mirror of each value inside a `NAME="..."` bash assignment. This guard
|
|
16
|
+
* asserts each mirror still matches its canonical value VERBATIM, so the two
|
|
17
|
+
* copies can never silently drift apart (the exact bug class ADR-2143 §3
|
|
18
|
+
* names for markdown-mirrored source-of-truth values, applied here to a
|
|
19
|
+
* bash-variable mirror instead of a pipe-table header).
|
|
20
|
+
*
|
|
21
|
+
* Modeled directly on `scripts/lint-table-schema-drift.cjs`: a standalone
|
|
22
|
+
* node script (not a node:test), pure detector function + `scanRepo` seam,
|
|
23
|
+
* wired into `lint:ci`, exit 0 clean / exit 1 + message on drift.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
|
|
29
|
+
// Canonical-source-constant name -> the exact bash variable name it mirrors
|
|
30
|
+
// inside gsd-core/workflows/pr-branch.md.
|
|
31
|
+
const PATTERN_VARS = {
|
|
32
|
+
TRANSIENT_DIRS_SRC: 'TRANSIENT_DIRS',
|
|
33
|
+
STRUCTURAL_RE_SRC: 'STRUCTURAL_RE',
|
|
34
|
+
MILESTONE_PHASES_RE_SRC: 'MILESTONE_PHASES_RE',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const WORKFLOW_REL_PATH = path.join('gsd-core', 'workflows', 'pr-branch.md');
|
|
38
|
+
|
|
39
|
+
/** Build the exact `NAME="value"` bash assignment line for one pattern. */
|
|
40
|
+
function buildAssignment(bashVar, value) {
|
|
41
|
+
return `${bashVar}="${value}"`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Pure: given `patterns` (a `{ SRC_CONST_NAME: value }` map, shaped like the
|
|
46
|
+
* `pr-branch-patterns.cjs` seam) and a `readFile(relPath) -> string|null`
|
|
47
|
+
* accessor, find every pattern whose exact `NAME="value"` assignment does
|
|
48
|
+
* not appear verbatim in the canonical workflow file. Returns
|
|
49
|
+
* `[{ constName, bashVar, expected, reason }]`; empty when clean.
|
|
50
|
+
*/
|
|
51
|
+
function findPrBranchPatternDrift(patterns, readFile, vars = PATTERN_VARS, workflowPath = WORKFLOW_REL_PATH) {
|
|
52
|
+
const violations = [];
|
|
53
|
+
const content = readFile(workflowPath);
|
|
54
|
+
|
|
55
|
+
for (const [constName, bashVar] of Object.entries(vars)) {
|
|
56
|
+
const value = patterns[constName];
|
|
57
|
+
if (typeof value !== 'string') {
|
|
58
|
+
violations.push({
|
|
59
|
+
constName,
|
|
60
|
+
bashVar,
|
|
61
|
+
expected: null,
|
|
62
|
+
reason: `${constName} missing or not a string in the pr-branch-patterns seam`,
|
|
63
|
+
});
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (content == null) {
|
|
68
|
+
violations.push({
|
|
69
|
+
constName,
|
|
70
|
+
bashVar,
|
|
71
|
+
expected: buildAssignment(bashVar, value),
|
|
72
|
+
reason: 'source file not found or unreadable',
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const expected = buildAssignment(bashVar, value);
|
|
78
|
+
if (!content.includes(expected)) {
|
|
79
|
+
violations.push({
|
|
80
|
+
constName,
|
|
81
|
+
bashVar,
|
|
82
|
+
expected,
|
|
83
|
+
reason: 'assignment not found verbatim in canonical workflow file',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return violations;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Load the built seam and scan the real repo tree. Returns the same shape as
|
|
92
|
+
* `findPrBranchPatternDrift`. If the seam hasn't been built yet (`npm run
|
|
93
|
+
* build:lib`), reports a single actionable violation rather than throwing.
|
|
94
|
+
*/
|
|
95
|
+
function scanRepo(root) {
|
|
96
|
+
const seamPath = path.join(root, 'gsd-core', 'bin', 'lib', 'pr-branch-patterns.cjs');
|
|
97
|
+
let seam;
|
|
98
|
+
try {
|
|
99
|
+
seam = require(seamPath);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
return [{
|
|
102
|
+
constName: null,
|
|
103
|
+
bashVar: null,
|
|
104
|
+
expected: null,
|
|
105
|
+
reason: `cannot load the pr-branch-patterns seam at ${path.relative(root, seamPath)} — run 'npm run build:lib' first (${e.message})`,
|
|
106
|
+
}];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const readFile = (relPath) => {
|
|
110
|
+
try {
|
|
111
|
+
return fs.readFileSync(path.join(root, relPath), 'utf8');
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return findPrBranchPatternDrift(seam, readFile);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function main() {
|
|
121
|
+
const root = path.join(__dirname, '..');
|
|
122
|
+
const violations = scanRepo(root);
|
|
123
|
+
if (violations.length === 0) {
|
|
124
|
+
process.stdout.write(
|
|
125
|
+
'ok pr-branch-pattern-drift: every pr-branch-patterns.cts constant appears verbatim in gsd-core/workflows/pr-branch.md\n',
|
|
126
|
+
);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
process.stderr.write(
|
|
130
|
+
'pr-branch-pattern-drift: pr-branch-patterns.cts constant(s) whose assignment is absent from gsd-core/workflows/pr-branch.md (ADR-4910 §8, #4605).\n',
|
|
131
|
+
);
|
|
132
|
+
process.stderr.write(
|
|
133
|
+
'Either update gsd-core/workflows/pr-branch.md to mirror the canonical value verbatim, or update\n'
|
|
134
|
+
+ 'src/pr-branch-patterns.cts to match — the two must never drift.\n',
|
|
135
|
+
);
|
|
136
|
+
for (const v of violations) {
|
|
137
|
+
const id = v.bashVar ? `${v.constName} (${v.bashVar})` : (v.constName ?? '?');
|
|
138
|
+
const expected = v.expected ? ` — expected ${JSON.stringify(v.expected)}` : '';
|
|
139
|
+
process.stderr.write(` ${id}: ${v.reason}${expected}\n`);
|
|
140
|
+
}
|
|
141
|
+
process.exitCode = 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (require.main === module) main();
|
|
145
|
+
|
|
146
|
+
module.exports = {
|
|
147
|
+
findPrBranchPatternDrift, scanRepo, buildAssignment, PATTERN_VARS, WORKFLOW_REL_PATH,
|
|
148
|
+
};
|