@opengsd/gsd-core 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +12 -0
- package/agents/gsd-executor.md +63 -35
- package/agents/gsd-plan-checker.md +76 -57
- package/agents/gsd-planner.md +14 -0
- package/agents/gsd-ui-checker.md +19 -3
- package/agents/gsd-ui-researcher.md +29 -0
- package/agents/gsd-verifier.md +23 -1
- package/bin/install.js +239 -67
- package/commands/gsd/execute-phase.md +1 -1
- package/commands/gsd/ns-workflow.md +2 -1
- package/commands/gsd/phase.md +1 -1
- package/commands/gsd/quick-batch.md +105 -0
- package/commands/gsd/surface.md +18 -8
- package/gsd-core/bin/gsd-tools.cjs +195 -50
- package/gsd-core/bin/lib/capability-activation.cjs +27 -0
- package/gsd-core/bin/lib/capability-registry.cjs +514 -114
- package/gsd-core/bin/lib/capability-state.cjs +7 -1
- package/gsd-core/bin/lib/capability-validator.cjs +120 -4
- package/gsd-core/bin/lib/capability-writer.cjs +14 -4
- package/gsd-core/bin/lib/check-command-router.cjs +85 -2
- package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/command-aliases.cjs +16 -0
- package/gsd-core/bin/lib/commands.cjs +337 -13
- package/gsd-core/bin/lib/config-loader.cjs +3 -0
- package/gsd-core/bin/lib/core-utils.cjs +34 -7
- package/gsd-core/bin/lib/decisions.cjs +213 -1
- package/gsd-core/bin/lib/edge-probe.cjs +14 -1
- package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
- package/gsd-core/bin/lib/frontmatter.cjs +137 -23
- package/gsd-core/bin/lib/gap-checker.cjs +22 -13
- package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
- package/gsd-core/bin/lib/host-integration.cjs +57 -5
- package/gsd-core/bin/lib/init-command-router.cjs +14 -0
- package/gsd-core/bin/lib/init.cjs +132 -15
- package/gsd-core/bin/lib/install-engine.cjs +184 -12
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
- package/gsd-core/bin/lib/install-profiles.cjs +22 -14
- package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
- package/gsd-core/bin/lib/io.cjs +35 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
- package/gsd-core/bin/lib/markdown-table.cjs +123 -0
- package/gsd-core/bin/lib/milestone.cjs +22 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
- package/gsd-core/bin/lib/phase-id.cjs +251 -9
- package/gsd-core/bin/lib/phase.cjs +774 -35
- package/gsd-core/bin/lib/plan-document.cjs +10 -0
- package/gsd-core/bin/lib/planning-snapshot.cjs +147 -20
- package/gsd-core/bin/lib/planning-workspace.cjs +103 -28
- package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
- package/gsd-core/bin/lib/quick-batch.cjs +840 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
- package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
- package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
- package/gsd-core/bin/lib/roadmap-parser.cjs +499 -26
- package/gsd-core/bin/lib/roadmap.cjs +187 -58
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +233 -33
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +286 -108
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -43
- package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
- package/gsd-core/bin/lib/smart-entry.cjs +7 -9
- package/gsd-core/bin/lib/state-document.cjs +30 -5
- package/gsd-core/bin/lib/state-md-schema.cjs +23 -13
- package/gsd-core/bin/lib/state-transition.cjs +333 -44
- package/gsd-core/bin/lib/state.cjs +684 -125
- package/gsd-core/bin/lib/surface.cjs +23 -8
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
- package/gsd-core/bin/lib/uat.cjs +1419 -515
- package/gsd-core/bin/lib/update-context.cjs +6 -2
- package/gsd-core/bin/lib/validate.cjs +230 -12
- package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
- package/gsd-core/bin/lib/verification.cjs +273 -12
- package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
- package/gsd-core/bin/lib/verify.cjs +346 -16
- package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +8 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
- package/gsd-core/references/agent-contracts.md +3 -3
- package/gsd-core/references/edge-probe.md +17 -13
- package/gsd-core/references/execute-mvp-tdd.md +18 -16
- package/gsd-core/references/execute-phase-response-language.md +6 -0
- package/gsd-core/references/executor-examples.md +42 -0
- package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
- package/gsd-core/references/mvp-concepts.md +2 -2
- package/gsd-core/references/plan-checker-examples.md +41 -0
- package/gsd-core/references/planner-antipatterns.md +25 -0
- package/gsd-core/references/planner-chunked.md +5 -1
- package/gsd-core/references/planner-coupling.md +42 -0
- package/gsd-core/references/planner-quick-batch.md +71 -0
- package/gsd-core/references/planner-reviews.md +47 -0
- package/gsd-core/references/planner-revision.md +75 -2
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/response-language-directive.md +9 -0
- package/gsd-core/references/revision-loop.md +118 -11
- package/gsd-core/references/tdd.md +14 -9
- package/gsd-core/references/verifier-evidence-gate.md +160 -0
- package/gsd-core/templates/phase-prompt.md +4 -0
- package/gsd-core/templates/verification-report.md +5 -0
- package/gsd-core/workflows/add-backlog.md +2 -0
- package/gsd-core/workflows/add-phase.md +2 -0
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/analyze-dependencies.md +2 -0
- package/gsd-core/workflows/audit-fix.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +2 -0
- package/gsd-core/workflows/audit-uat.md +2 -0
- package/gsd-core/workflows/autonomous.md +2 -0
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +15 -13
- package/gsd-core/workflows/code-review-fix.md +2 -0
- package/gsd-core/workflows/code-review.md +73 -31
- package/gsd-core/workflows/complete-milestone.md +13 -4
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
- package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
- package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase-power.md +2 -0
- package/gsd-core/workflows/discuss-phase.md +1 -1
- package/gsd-core/workflows/do.md +43 -13
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +2 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +17 -1
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
- package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
- package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
- package/gsd-core/workflows/execute-phase.md +32 -14
- package/gsd-core/workflows/execute-plan.md +8 -8
- package/gsd-core/workflows/explore.md +2 -0
- package/gsd-core/workflows/extract-learnings.md +2 -0
- package/gsd-core/workflows/fast.md +6 -0
- package/gsd-core/workflows/forensics.md +2 -0
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/brief.md +2 -0
- package/gsd-core/workflows/help/modes/default.md +2 -0
- package/gsd-core/workflows/help/modes/full.md +12 -0
- package/gsd-core/workflows/help/modes/topic.md +2 -0
- package/gsd-core/workflows/help.md +2 -0
- package/gsd-core/workflows/import.md +3 -3
- package/gsd-core/workflows/inbox.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +2 -0
- package/gsd-core/workflows/list-phase-assumptions.md +2 -0
- package/gsd-core/workflows/list-seeds.md +2 -0
- package/gsd-core/workflows/list-workspaces.md +2 -0
- package/gsd-core/workflows/manager.md +3 -3
- package/gsd-core/workflows/map-codebase.md +2 -0
- package/gsd-core/workflows/milestone-summary.md +2 -0
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +1 -1
- package/gsd-core/workflows/new-project.md +5 -3
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +2 -0
- package/gsd-core/workflows/node-repair.md +2 -0
- package/gsd-core/workflows/note.md +2 -0
- package/gsd-core/workflows/onboard.md +1 -1
- package/gsd-core/workflows/pause-work.md +19 -4
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
- package/gsd-core/workflows/plan-phase.md +130 -12
- package/gsd-core/workflows/plan-review-convergence.md +102 -10
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/pr-branch.md +11 -3
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
- package/gsd-core/workflows/progress.md +25 -3
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
- package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
- package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
- package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
- package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
- package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
- package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
- package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
- package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
- package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
- package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
- package/gsd-core/workflows/quick-batch.md +203 -0
- package/gsd-core/workflows/quick.md +13 -3
- package/gsd-core/workflows/reapply-patches.md +2 -0
- package/gsd-core/workflows/remove-phase.md +2 -0
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +6 -2
- package/gsd-core/workflows/review.md +215 -10
- package/gsd-core/workflows/scan.md +2 -0
- package/gsd-core/workflows/section-manifest.json +12 -0
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/session-report.md +2 -0
- package/gsd-core/workflows/settings-advanced.md +2 -0
- package/gsd-core/workflows/settings-integrations.md +9 -8
- package/gsd-core/workflows/settings.md +1 -1
- package/gsd-core/workflows/ship.md +10 -10
- package/gsd-core/workflows/sketch-wrap-up.md +2 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/spec-phase.md +24 -19
- package/gsd-core/workflows/spike-wrap-up.md +2 -0
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +2 -0
- package/gsd-core/workflows/sync-skills.md +12 -4
- package/gsd-core/workflows/thread.md +2 -0
- package/gsd-core/workflows/transition.md +2 -0
- package/gsd-core/workflows/ui-phase.md +26 -5
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +2 -0
- package/gsd-core/workflows/undo.md +1 -1
- package/gsd-core/workflows/update.md +41 -38
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +49 -3
- package/hooks/dist/gsd-check-update-worker.js +19 -2
- package/hooks/dist/gsd-context-monitor.js +283 -12
- package/hooks/dist/gsd-node-runner.sh +1 -0
- package/hooks/dist/gsd-prompt-guard.js +30 -5
- package/hooks/dist/gsd-read-guard.js +2 -0
- package/hooks/dist/gsd-read-injection-scanner.js +5 -5
- package/hooks/dist/gsd-secret-read-guard.js +1079 -0
- package/hooks/dist/gsd-statusline.js +7 -3
- package/hooks/dist/gsd-validate-commit.sh +444 -7
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/git-cmd.js +210 -1
- package/hooks/dist/lib/injection-patterns.js +36 -6
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-check-update-worker.js +19 -2
- package/hooks/gsd-context-monitor.js +283 -12
- package/hooks/gsd-node-runner.sh +1 -0
- package/hooks/gsd-prompt-guard.js +30 -5
- package/hooks/gsd-read-guard.js +2 -0
- package/hooks/gsd-read-injection-scanner.js +5 -5
- package/hooks/gsd-secret-read-guard.js +1079 -0
- package/hooks/gsd-statusline.js +7 -3
- package/hooks/gsd-validate-commit.sh +444 -7
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/hooks.json +6 -0
- package/hooks/lib/git-cmd.js +210 -1
- package/hooks/lib/injection-patterns.js +36 -6
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +5 -5
- package/scripts/build-hooks.js +11 -4
- package/scripts/ci-test-scope.cjs +7 -0
- package/scripts/docs-guard-registry.cjs +10 -0
- package/scripts/gen-loop-host-contract.cjs +67 -15
- package/scripts/lib/shellcheck-fetch.cjs +247 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
- package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
- package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +5 -0
- package/scripts/lint-phase-enumeration-drift.cjs +24 -6
- package/scripts/lint-phase-id-drift.cjs +133 -8
- package/scripts/lint-portable-grep.cjs +176 -0
- package/scripts/lint-response-language-coverage.cjs +524 -0
- package/scripts/lint-test-file-count.allowlist.json +3 -1
- package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
- package/scripts/lint-workflow-shellcheck.cjs +614 -0
- package/scripts/npm-audit-baseline.cjs +376 -0
- package/scripts/prompt-injection-scan.sh +8 -0
- package/scripts/require-issue-link-policy.cjs +16 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-ns-workflow/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -1
- package/skills/gsd-quick-batch/SKILL.md +105 -0
- package/skills/gsd-surface/SKILL.md +18 -8
- package/vscode/package.json +1 -1
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* shellcheck-fetch.cjs
|
|
5
|
+
*
|
|
6
|
+
* Dependency-free replacement for the `shellcheck` npm package (removed in
|
|
7
|
+
* #4120 — its extraction step pulled in `decompress@4.2.1`, which carries an
|
|
8
|
+
* unpatched CRITICAL zip-slip vulnerability, GHSA-mp2f-45pm-3cg9, CVSS 9.1,
|
|
9
|
+
* plus two moderate findings, with no patched version available upstream).
|
|
10
|
+
*
|
|
11
|
+
* This module fetches a PINNED koalaman/shellcheck release directly from
|
|
12
|
+
* GitHub releases (never "latest" — see SHELLCHECK_VERSION below), extracts
|
|
13
|
+
* the single `shellcheck` binary from the release's `.tar.gz` asset using
|
|
14
|
+
* only Node's built-in `https`/`zlib` modules plus a small hand-written tar
|
|
15
|
+
* reader (no third-party archive library), and caches the extracted binary
|
|
16
|
+
* for reuse across runs.
|
|
17
|
+
*
|
|
18
|
+
* Zip-slip defense: unlike `decompress`, which wrote extracted files using
|
|
19
|
+
* PATHS TAKEN FROM THE ARCHIVE (the exact defect class in GHSA-mp2f-45pm-
|
|
20
|
+
* 3cg9 — a malicious archive entry named e.g. `../../etc/passwd` gets
|
|
21
|
+
* written there verbatim), this reader NEVER uses an archive-supplied name
|
|
22
|
+
* as a filesystem path. `extractFileFromTar` only ever returns the matched
|
|
23
|
+
* entry's raw byte content; the caller (`resolveShellcheckBin`) writes those
|
|
24
|
+
* bytes to a path it constructs itself (`<cacheDir>/shellcheck`), and the
|
|
25
|
+
* archive's own name field is used only for a string-equality/suffix CHECK
|
|
26
|
+
* (`name === targetName || name.endsWith('/' + targetName)`), never
|
|
27
|
+
* interpolated into a path passed to `fs.writeFileSync`/`fs.mkdirSync`/etc.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require('node:fs');
|
|
31
|
+
const path = require('node:path');
|
|
32
|
+
const zlib = require('node:zlib');
|
|
33
|
+
const https = require('node:https');
|
|
34
|
+
const { ExitError } = require('./cli-exit.cjs');
|
|
35
|
+
|
|
36
|
+
// Pinned explicitly — verified against `koalaman/shellcheck`'s GitHub
|
|
37
|
+
// releases API as the current latest tag at the time this was written
|
|
38
|
+
// (2026-08-31). Never resolved dynamically ("latest") — a moving target
|
|
39
|
+
// would make this lint's exact ShellCheck version, and therefore its exact
|
|
40
|
+
// finding set against scripts/lint-workflow-shellcheck-baseline.json,
|
|
41
|
+
// non-reproducible across runs/machines/CI.
|
|
42
|
+
const SHELLCHECK_VERSION = 'v0.11.0';
|
|
43
|
+
|
|
44
|
+
const ROOT = path.join(__dirname, '..', '..');
|
|
45
|
+
const CACHE_DIR = path.join(ROOT, 'node_modules', '.cache', 'shellcheck', SHELLCHECK_VERSION);
|
|
46
|
+
const CACHED_BIN_PATH = path.join(CACHE_DIR, 'shellcheck');
|
|
47
|
+
|
|
48
|
+
// process.arch -> the arch token ShellCheck's release asset names use.
|
|
49
|
+
// Only the two architectures that actually matter for this repo (per
|
|
50
|
+
// .github/workflows/test.yml: the lint-tests job that runs this script only
|
|
51
|
+
// runs on ubuntu-latest, which is x86_64; and Apple Silicon dev machines are
|
|
52
|
+
// aarch64) are supported — anything else fails with a clear error rather
|
|
53
|
+
// than guessing.
|
|
54
|
+
const ARCH_MAP = { x64: 'x86_64', arm64: 'aarch64' };
|
|
55
|
+
|
|
56
|
+
const MAX_REDIRECTS = 5;
|
|
57
|
+
|
|
58
|
+
// Bounds each individual HTTP hop (the initial request AND every redirect
|
|
59
|
+
// hop get their own fresh 30s budget, rather than one shared budget across
|
|
60
|
+
// the whole redirect chain) — a stalled connection on any single hop is
|
|
61
|
+
// caught in a bounded time, mirroring lint-workflow-shellcheck.cjs's own
|
|
62
|
+
// SHELLCHECK_TIMEOUT_MS bound on the ShellCheck subprocess. A bare `timeout`
|
|
63
|
+
// option on the request does NOT abort it by itself — Node only emits a
|
|
64
|
+
// 'timeout' event, which must be handled by destroying the request (see the
|
|
65
|
+
// `req.on('timeout', ...)` below).
|
|
66
|
+
const DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Issue one real HTTPS request. Exists as its own function purely so tests
|
|
70
|
+
* can inject a fake in its place (see `httpsGetFollowingRedirects`'s
|
|
71
|
+
* `requestFn` parameter) — production callers never pass an override, so
|
|
72
|
+
* the real download path always uses this exact implementation.
|
|
73
|
+
*/
|
|
74
|
+
function defaultRequestFn(url, options, callback) {
|
|
75
|
+
return https.get(url, options, callback);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* GET `url` following HTTP redirects manually — `https.get` does NOT follow
|
|
80
|
+
* redirects automatically, and GitHub release asset URLs redirect through
|
|
81
|
+
* `objects.githubusercontent.com`. Resolves with the full response body as a
|
|
82
|
+
* Buffer once a 200 response is received.
|
|
83
|
+
*
|
|
84
|
+
* `requestFn` defaults to a real `https.get`-based transport
|
|
85
|
+
* (`defaultRequestFn`) and is only ever overridden in tests, so calling this
|
|
86
|
+
* with zero/one arg from `resolveShellcheckBin` is unchanged behavior.
|
|
87
|
+
*/
|
|
88
|
+
function httpsGetFollowingRedirects(url, redirectsLeft = MAX_REDIRECTS, requestFn = defaultRequestFn) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const req = requestFn(
|
|
91
|
+
url,
|
|
92
|
+
{ headers: { 'User-Agent': 'gsd-core-shellcheck-fetch' }, timeout: DOWNLOAD_TIMEOUT_MS },
|
|
93
|
+
(res) => {
|
|
94
|
+
const status = res.statusCode || 0;
|
|
95
|
+
if (status >= 300 && status < 400 && res.headers.location) {
|
|
96
|
+
res.resume(); // drain so the socket can be reused/closed
|
|
97
|
+
if (redirectsLeft <= 0) {
|
|
98
|
+
reject(new Error(`too many redirects fetching ${url}`));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const next = new URL(res.headers.location, url).toString();
|
|
102
|
+
httpsGetFollowingRedirects(next, redirectsLeft - 1, requestFn).then(resolve, reject);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (status !== 200) {
|
|
106
|
+
res.resume();
|
|
107
|
+
reject(new Error(`unexpected HTTP ${status} fetching ${url}`));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const chunks = [];
|
|
111
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
112
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
113
|
+
res.on('error', reject);
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
req.on('error', reject);
|
|
117
|
+
// `timeout` in the options above only ARMS a timer — Node emits a
|
|
118
|
+
// 'timeout' event on the request but does not abort it. Without this
|
|
119
|
+
// handler the request (and this Promise) would hang indefinitely past
|
|
120
|
+
// the configured bound on a stalled connection.
|
|
121
|
+
req.on('timeout', () => {
|
|
122
|
+
req.destroy(new Error(`timed out after ${DOWNLOAD_TIMEOUT_MS}ms fetching ${url}`));
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Find the entry named (or path-ending-in) `targetName` inside a raw
|
|
129
|
+
* (already gunzipped) POSIX tar byte stream and return its content as a
|
|
130
|
+
* Buffer, or `null` if not found.
|
|
131
|
+
*
|
|
132
|
+
* Tar format: a sequence of 512-byte headers — name at offset 0/length 100,
|
|
133
|
+
* size at offset 124/length 12 (octal ASCII), typeflag at offset 156 — each
|
|
134
|
+
* followed by that many content bytes, padded up to the next 512-byte
|
|
135
|
+
* boundary, terminated by an all-zero 512-byte block. This deliberately
|
|
136
|
+
* implements only enough to locate ONE known entry name (no general
|
|
137
|
+
* multi-file extraction, no symlink handling, no GNU long-name `@LongLink`
|
|
138
|
+
* entries — ShellCheck's own release tarballs never need them) — see this
|
|
139
|
+
* module's header comment for why the entry's NAME is never used as a
|
|
140
|
+
* filesystem path.
|
|
141
|
+
*/
|
|
142
|
+
function extractFileFromTar(buffer, targetName) {
|
|
143
|
+
let offset = 0;
|
|
144
|
+
while (offset + 512 <= buffer.length) {
|
|
145
|
+
const header = buffer.subarray(offset, offset + 512);
|
|
146
|
+
if (header.every((b) => b === 0)) break; // end-of-archive marker
|
|
147
|
+
const name = header.subarray(0, 100).toString('utf8').replace(/\0.*$/, '');
|
|
148
|
+
const sizeRaw = header.subarray(124, 136).toString('utf8').replace(/\0.*$/, '').trim();
|
|
149
|
+
const size = sizeRaw === '' ? 0 : parseInt(sizeRaw, 8);
|
|
150
|
+
const typeflag = String.fromCharCode(header[156]);
|
|
151
|
+
const dataStart = offset + 512;
|
|
152
|
+
const isRegularFile = typeflag === '0' || typeflag === '\0';
|
|
153
|
+
if (isRegularFile && (name === targetName || name.endsWith(`/${targetName}`))) {
|
|
154
|
+
return buffer.subarray(dataStart, dataStart + size);
|
|
155
|
+
}
|
|
156
|
+
const contentBlocks = Math.ceil(size / 512);
|
|
157
|
+
offset = dataStart + contentBlocks * 512;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resolve the local path to a working, executable `shellcheck` binary,
|
|
164
|
+
* downloading and caching the pinned release on first use. Subsequent calls
|
|
165
|
+
* (same version) reuse the cached binary with no network activity — mirrors
|
|
166
|
+
* the `fs.accessSync(bin, F_OK | X_OK)` cache-check pattern already used by
|
|
167
|
+
* this script's own `runShellcheck`.
|
|
168
|
+
*/
|
|
169
|
+
async function resolveShellcheckBin() {
|
|
170
|
+
try {
|
|
171
|
+
fs.accessSync(CACHED_BIN_PATH, fs.constants.F_OK | fs.constants.X_OK);
|
|
172
|
+
return CACHED_BIN_PATH;
|
|
173
|
+
} catch {
|
|
174
|
+
// not cached yet — fall through to download
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (process.platform === 'win32') {
|
|
178
|
+
throw new ExitError(
|
|
179
|
+
1,
|
|
180
|
+
'lint-workflow-shellcheck: automatic ShellCheck download is not supported on Windows yet ' +
|
|
181
|
+
'(this lint only ever runs in the ubuntu-latest lint-tests CI job — see .github/workflows/test.yml — ' +
|
|
182
|
+
'so this is an honest platform gap, not expected to be hit in CI).',
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
const platform = process.platform === 'darwin' || process.platform === 'linux' ? process.platform : null;
|
|
186
|
+
if (!platform) {
|
|
187
|
+
throw new ExitError(
|
|
188
|
+
1,
|
|
189
|
+
`lint-workflow-shellcheck: unsupported platform '${process.platform}' for ShellCheck auto-download ` +
|
|
190
|
+
`(supported: linux, darwin).`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const arch = ARCH_MAP[process.arch];
|
|
194
|
+
if (!arch) {
|
|
195
|
+
throw new ExitError(
|
|
196
|
+
1,
|
|
197
|
+
`lint-workflow-shellcheck: unsupported architecture '${process.arch}' for ShellCheck auto-download ` +
|
|
198
|
+
`(supported: x86_64 [node arch 'x64'], aarch64 [node arch 'arm64']).`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const assetName = `shellcheck-${SHELLCHECK_VERSION}.${platform}.${arch}.tar.gz`;
|
|
203
|
+
const url = `https://github.com/koalaman/shellcheck/releases/download/${SHELLCHECK_VERSION}/${assetName}`;
|
|
204
|
+
|
|
205
|
+
let gz;
|
|
206
|
+
try {
|
|
207
|
+
gz = await httpsGetFollowingRedirects(url);
|
|
208
|
+
} catch (e) {
|
|
209
|
+
throw new ExitError(1, `lint-workflow-shellcheck: failed to download ShellCheck (${url}): ${e.message}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let tarBuf;
|
|
213
|
+
try {
|
|
214
|
+
tarBuf = zlib.gunzipSync(gz);
|
|
215
|
+
} catch (e) {
|
|
216
|
+
throw new ExitError(1, `lint-workflow-shellcheck: failed to gunzip downloaded ShellCheck archive: ${e.message}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const entry = extractFileFromTar(tarBuf, 'shellcheck');
|
|
220
|
+
if (!entry) {
|
|
221
|
+
throw new ExitError(
|
|
222
|
+
1,
|
|
223
|
+
`lint-workflow-shellcheck: could not find a 'shellcheck' entry inside downloaded archive ${assetName}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
228
|
+
// Write to a per-process temp path and rename into place — avoids any
|
|
229
|
+
// other concurrent invocation observing (and trying to execute) a
|
|
230
|
+
// partially-written binary at the real cache path.
|
|
231
|
+
const tmpPath = path.join(CACHE_DIR, `.shellcheck.tmp-${process.pid}`);
|
|
232
|
+
fs.writeFileSync(tmpPath, entry);
|
|
233
|
+
fs.chmodSync(tmpPath, 0o755);
|
|
234
|
+
fs.renameSync(tmpPath, CACHED_BIN_PATH);
|
|
235
|
+
|
|
236
|
+
return CACHED_BIN_PATH;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = {
|
|
240
|
+
SHELLCHECK_VERSION,
|
|
241
|
+
CACHED_BIN_PATH,
|
|
242
|
+
MAX_REDIRECTS,
|
|
243
|
+
DOWNLOAD_TIMEOUT_MS,
|
|
244
|
+
extractFileFromTar,
|
|
245
|
+
httpsGetFollowingRedirects,
|
|
246
|
+
resolveShellcheckBin,
|
|
247
|
+
};
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
"tests/code-review-pipeline-regression.test.cjs :: source-text-is-the-product",
|
|
25
25
|
"tests/code-review.test.cjs :: source-text-is-the-product",
|
|
26
26
|
"tests/codebuddy-install.test.cjs :: source-text-is-the-product",
|
|
27
|
-
"tests/codex-config.test.cjs :: source-text-is-the-product",
|
|
28
27
|
"tests/command-contract.test.cjs :: source-text-is-the-product",
|
|
29
28
|
"tests/commands.test.cjs :: source-text-is-the-product",
|
|
30
29
|
"tests/config-field-docs.test.cjs :: docs-parity",
|
|
@@ -52,7 +51,6 @@
|
|
|
52
51
|
"tests/frontmatter-cli.test.cjs :: source-text-is-the-product",
|
|
53
52
|
"tests/gates-taxonomy.test.cjs :: source-text-is-the-product",
|
|
54
53
|
"tests/git-base-branch.test.cjs :: source-text-is-the-product",
|
|
55
|
-
"tests/gsd-check-update-worker-platform-gate.test.cjs :: structural-regression-guard",
|
|
56
54
|
"tests/gsd-researcher-app-aware.test.cjs :: source-text-is-the-product",
|
|
57
55
|
"tests/gsd-researcher-flow-diagram.test.cjs :: source-text-is-the-product",
|
|
58
56
|
"tests/gsd-settings-advanced.test.cjs :: source-text-is-the-product",
|
|
@@ -74,13 +72,11 @@
|
|
|
74
72
|
"tests/next-safety-gates.test.cjs :: source-text-is-the-product",
|
|
75
73
|
"tests/next-up-clear-order.test.cjs :: source-text-is-the-product",
|
|
76
74
|
"tests/no-hardcoded-home-gsd-tools.test.cjs :: source-text-is-the-product",
|
|
77
|
-
"tests/orphaned-hooks.test.cjs :: structural-regression-guard",
|
|
78
75
|
"tests/package-legitimacy-gate.test.cjs :: source-text-is-the-product",
|
|
79
76
|
"tests/parallel-dependent-plans.test.cjs :: source-text-is-the-product",
|
|
80
77
|
"tests/path-replacement.test.cjs :: source-text-is-the-product",
|
|
81
78
|
"tests/phase.test.cjs :: source-text-is-the-product",
|
|
82
79
|
"tests/phase6-capability-docs.test.cjs :: source-text-is-the-product",
|
|
83
|
-
"tests/phase6-capstone-conformance.test.cjs :: source-text-is-the-product",
|
|
84
80
|
"tests/phase6-planning-capabilities.test.cjs :: source-text-is-the-product",
|
|
85
81
|
"tests/plan-bounce.test.cjs :: source-text-is-the-product",
|
|
86
82
|
"tests/plan-phase-drift-guard.test.cjs :: source-text-is-the-product",
|
|
@@ -105,7 +101,6 @@
|
|
|
105
101
|
"tests/research-agent-profiles.test.cjs :: source-text-is-the-product",
|
|
106
102
|
"tests/roadmap.test.cjs :: source-text-is-the-product",
|
|
107
103
|
"tests/runtime-launcher-parity.test.cjs :: structural-regression-guard",
|
|
108
|
-
"tests/runtime-name-policy.test.cjs :: source-text-is-the-product",
|
|
109
104
|
"tests/scan-command.test.cjs :: source-text-is-the-product",
|
|
110
105
|
"tests/secret-scan-lint.security.test.cjs :: source-text-is-the-product",
|
|
111
106
|
"tests/secure-phase.test.cjs :: source-text-is-the-product",
|
|
@@ -113,7 +108,6 @@
|
|
|
113
108
|
"tests/security-scan.security.test.cjs :: source-text-is-the-product",
|
|
114
109
|
"tests/seed-scan-new-milestone.test.cjs :: source-text-is-the-product",
|
|
115
110
|
"tests/settings-integrations.test.cjs :: source-text-is-the-product",
|
|
116
|
-
"tests/settings-jsonc.test.cjs :: structural-regression-guard",
|
|
117
111
|
"tests/skill-frontmatter-contract.test.cjs :: source-text-is-the-product",
|
|
118
112
|
"tests/spawn-liveness-banner.test.cjs :: source-text-is-the-product",
|
|
119
113
|
"tests/state.test.cjs :: source-text-is-the-product",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* `docs-guard-exempt:` marker for its specific reason.
|
|
32
32
|
*/
|
|
33
33
|
const DOCS_GUARD_EXEMPT_BASELINE = [
|
|
34
|
+
'adr-612-bracket-phase-counting.test.cjs',
|
|
34
35
|
'adr-parser.test.cjs',
|
|
35
36
|
'adr-parser.unit.test.cjs',
|
|
36
37
|
'agent-marker-documentation-guard.test.cjs',
|
|
@@ -103,6 +104,10 @@ const DOCS_GUARD_EXEMPT_BASELINE = [
|
|
|
103
104
|
* exact same scan.
|
|
104
105
|
*/
|
|
105
106
|
const DOCS_GUARD_EXEMPT_DOCS_PATHS = {
|
|
107
|
+
// #2761: cites docs/adr/612-bracket-phase-id-convention.md in an explanatory
|
|
108
|
+
// comment describing ADR-612's Decision 1; the file never reads that (or any)
|
|
109
|
+
// docs/ file — every read call it makes targets a tmpdir .planning fixture.
|
|
110
|
+
'adr-612-bracket-phase-counting.test.cjs': ['docs/adr/612-bracket-phase-id-convention.md'],
|
|
106
111
|
'adr-parser.test.cjs': ['docs/adr/0001.md', 'docs/adr/0002.md', 'docs/adr/0010.md', 'docs/adr/NNNN.md'],
|
|
107
112
|
'adr-parser.unit.test.cjs': ['docs/adr/0001.md', 'docs/adr/0099.md', 'docs/adr/NNNN.md'],
|
|
108
113
|
'agent-marker-documentation-guard.test.cjs': ['docs/reference', 'docs/reference/workflow-fragments.md'],
|
|
@@ -84,9 +84,13 @@
|
|
|
84
84
|
* shape as `collectDiskPhases` and the `audit.cts` scanners; it must see
|
|
85
85
|
* every phase directory regardless of milestone window to catch a
|
|
86
86
|
* naming/duplicate defect wherever it lives.
|
|
87
|
-
* - `src/verify.cts` `
|
|
87
|
+
* - `src/verify.cts` `resolvePhaseDirByToken`: resolves ONE caller-supplied
|
|
88
88
|
* `phase` argument to its directory (falling back to an exact-name
|
|
89
89
|
* match) — a single-phase LOOKUP, not a current-milestone enumeration.
|
|
90
|
+
* Originally `cmdVerifySchemaDrift`'s own inline block; #3348 lifted it
|
|
91
|
+
* into this shared helper (also used by the new `cmdVerifyContextDrift`)
|
|
92
|
+
* without changing what question it asks, so the exemption moved with
|
|
93
|
+
* the call site rather than multiplying.
|
|
90
94
|
* - `src/init.cts` `detectHasPriorPhases`: answers "has this project EVER
|
|
91
95
|
* completed a phase", explicitly excluding the current one. A history
|
|
92
96
|
* probe across all milestones, not a current-milestone enumeration.
|
|
@@ -128,8 +132,10 @@
|
|
|
128
132
|
* `--include-archived` merge are phase LOCATION and archive
|
|
129
133
|
* enumeration, not current-milestone enumeration; both legitimately
|
|
130
134
|
* read the physical set. Its ENUMERATION path routes through the owner.
|
|
131
|
-
* - `src/roadmap-parser.cts` `getMilestonePhaseFilter` and its
|
|
132
|
-
* set-building
|
|
135
|
+
* - `src/roadmap-parser.cts` `getMilestonePhaseFilter` and its shared
|
|
136
|
+
* set-building implementation `scanMilestonePhaseIdSets` (the public
|
|
137
|
+
* `scanMilestonePhaseIds` wrapper remains a directly iterable Set; the
|
|
138
|
+
* same two heading/
|
|
133
139
|
* #3577 `collectTablePhaseRows` — the table-scan sibling feeding the same
|
|
134
140
|
* membership set; its local 999-only exclusion mirrors the owner's
|
|
135
141
|
* deliberate NOT-isSentinelPhaseId choice (a leading 0 is a real decimal
|
|
@@ -142,6 +148,18 @@
|
|
|
142
148
|
* ("00.1" is a real phase, not milestone 0). This scan asks a narrower
|
|
143
149
|
* question — "which phase ids does this milestone's window declare" —
|
|
144
150
|
* where only the 999 icebox range is excluded.
|
|
151
|
+
* - `src/state.cts` `countRoadmapPhaseHeadings`: its bracket branch composes
|
|
152
|
+
* bracket-milestone sentinel detection through `isSentinelPhaseId`, but
|
|
153
|
+
* retains a separate bare-token `999` rule. That rule intentionally does
|
|
154
|
+
* NOT use the convention-blind canonical predicate: the latter also reads
|
|
155
|
+
* leading `0`/`00.1` as milestone-0 sentinels, while a legacy-spelled bare
|
|
156
|
+
* zero token inside an opted-in bracket project is a real mid-migration
|
|
157
|
+
* phase. This is the state-counter twin of the roadmap-parser exemption.
|
|
158
|
+
* The guard has no per-detector exemption scope: it ORs the enumeration
|
|
159
|
+
* and sentinel detectors before consulting this function-only map. The
|
|
160
|
+
* whole-function exemption is therefore accepted; it is bounded because
|
|
161
|
+
* this counter performs no phases-directory `readdirSync` and needs the
|
|
162
|
+
* exemption only for its intentional bare-token `999` sentinel literal.
|
|
145
163
|
* - `src/state.cts` `cmdStateValidate` ("Gate 1: Validate STATE.md against
|
|
146
164
|
* filesystem"): resolves ONE directory — the disk match for STATE.md's
|
|
147
165
|
* own `Current Phase` field — by prefix, a single-phase LOOKUP, not an
|
|
@@ -291,7 +309,7 @@ const OWNER_FILES = new Set([
|
|
|
291
309
|
// `lint-milestone-window-drift.cjs`'s FUNCTION_SCOPED_EXEMPTIONS mechanism.
|
|
292
310
|
// See the header comment for the full written reason behind each entry.
|
|
293
311
|
const FUNCTION_SCOPED_EXEMPTIONS = new Map([
|
|
294
|
-
[path.join('src', 'verify.cts'), new Set(['cmdValidateHealth', '
|
|
312
|
+
[path.join('src', 'verify.cts'), new Set(['cmdValidateHealth', 'resolvePhaseDirByToken'])],
|
|
295
313
|
[path.join('src', 'init.cts'), new Set(['detectHasPriorPhases', 'detectUiPhaseActive'])],
|
|
296
314
|
[path.join('src', 'milestone.cts'), new Set(['archivePhaseDirectories', 'cmdMilestoneComplete', 'cmdPhasesClear'])],
|
|
297
315
|
// #3849: collectSiblingWorktreePhaseNums reads a SIBLING worktree's phases dir —
|
|
@@ -300,10 +318,10 @@ const FUNCTION_SCOPED_EXEMPTIONS = new Map([
|
|
|
300
318
|
[path.join('src', 'phase.cts'), new Set(['cmdPhasesList', 'cmdPhaseNextDecimal', 'cmdPhasePlanIndex', 'cmdPhaseInsert', 'renameDecimalPhases', 'renameIntegerPhases', 'collectSiblingWorktreePhaseNums'])],
|
|
301
319
|
[path.join('src', 'audit.cts'), new Set(['listAuditPhaseTargets'])],
|
|
302
320
|
[path.join('src', 'commands.cts'), new Set(['cmdHistoryDigest'])],
|
|
303
|
-
[path.join('src', 'state.cts'), new Set(['cmdStateValidate', 'cmdStateSync', 'cmdStateRebuild'])],
|
|
321
|
+
[path.join('src', 'state.cts'), new Set(['cmdStateValidate', 'cmdStateSync', 'cmdStateRebuild', 'countRoadmapPhaseHeadings'])],
|
|
304
322
|
[path.join('src', 'roadmap-upgrade.cts'), new Set(['computeMigrationPlan'])],
|
|
305
323
|
[path.join('src', 'smart-entry.cts'), new Set(['detectVerifyFailed'])],
|
|
306
|
-
[path.join('src', 'roadmap-parser.cts'), new Set(['getMilestonePhaseFilter', '
|
|
324
|
+
[path.join('src', 'roadmap-parser.cts'), new Set(['getMilestonePhaseFilter', 'scanMilestonePhaseIdSets', 'collectTablePhaseRows'])],
|
|
307
325
|
[path.join('src', 'planning-snapshot.cts'), new Set(['buildAllPhaseDirNamesField'])],
|
|
308
326
|
]);
|
|
309
327
|
|
|
@@ -53,6 +53,35 @@ const TOKEN_DRIFT_RE = /(?:\\{1,2}d|\[0-9\])\+\[A-Z(?:a-z)?\]\??\(\?:(?:\\{1,2}\
|
|
|
53
53
|
const OWNER_RE = /^\s*\/\/.*phase-id-owner:/;
|
|
54
54
|
const CANON_REF = 'PHASE_NUMBER_TOKEN_SOURCE';
|
|
55
55
|
|
|
56
|
+
// #2761 M3 (trek-e review): the SECOND grammar this seam owns — the BRACKET
|
|
57
|
+
// project-code class of `[CODE.MM]`, spelled `[A-Z][A-Z0-9_]*` (with its
|
|
58
|
+
// case-widened `[A-Za-z]`/`[A-Za-z0-9_]` variant tolerated so a trivial rewrite
|
|
59
|
+
// does not evade the rule). The token guard above only ever knew the phase-
|
|
60
|
+
// NUMBER grammar, so three files re-typed this class verbatim — roadmap-parser's
|
|
61
|
+
// bracket-fallback selector, state's `isMilestoneBounded`, verify's
|
|
62
|
+
// `checkBracketCoherence` — and `check:phase-id-drift` reported clean the whole
|
|
63
|
+
// time. That is the blind spot which let #2761's own "no token literal outside
|
|
64
|
+
// src/phase-id.cts" gate pass while being violated. Build from
|
|
65
|
+
// `BRACKET_PROJECT_CODE_SRC`, `BRACKET_ID_SRC`, `bracketMilestoneIntroSrcFor`
|
|
66
|
+
// or `BRACKET_MILESTONE_INTRO_CAPTURING_SRC` instead.
|
|
67
|
+
const BRACKET_CODE_DRIFT_RE = /\[A-Z(?:a-z)?\]\[A-Z(?:a-z)?0-9_\]\*/;
|
|
68
|
+
|
|
69
|
+
// This rule has NO counterpart to the token rule's `line.includes(CANON_REF)`
|
|
70
|
+
// escape, and that omission is the point.
|
|
71
|
+
//
|
|
72
|
+
// That escape is LINE-level: a line naming the canonical source anywhere on it
|
|
73
|
+
// is taken as built-from-the-owner. verify.cts's copy read
|
|
74
|
+
//
|
|
75
|
+
// new RegExp(`^\\[[A-Z][A-Z0-9_]*\\.(${BRACKET_MILESTONE_NUMERIC_SRC})\\]`, 'i')
|
|
76
|
+
//
|
|
77
|
+
// — an owner reference for the MILESTONE field sharing a line with a re-typed
|
|
78
|
+
// PROJECT-CODE class. A line-level escape waves that through, so a bracket rule
|
|
79
|
+
// that copied it would have kept reporting clean on the very site under review.
|
|
80
|
+
// Partial ownership IS the drift. Only a `// phase-id-owner:` sanction
|
|
81
|
+
// suppresses this rule, and a sanction has to state which half is deliberate.
|
|
82
|
+
const BRACKET_OWNER_HINT =
|
|
83
|
+
'BRACKET_PROJECT_CODE_SRC / BRACKET_ID_SRC / bracketMilestoneIntroSrcFor / BRACKET_MILESTONE_INTRO_CAPTURING_SRC';
|
|
84
|
+
|
|
56
85
|
/**
|
|
57
86
|
* Pure: find every literal re-derivation of the canonical phase-number token in
|
|
58
87
|
* `text` that is NOT sanctioned. A site is sanctioned when the nearest preceding
|
|
@@ -79,6 +108,28 @@ function findPhaseIdRegexDrift(text) {
|
|
|
79
108
|
return out;
|
|
80
109
|
}
|
|
81
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Pure: find every literal re-derivation of the BRACKET project-code grammar in
|
|
113
|
+
* `text` that is NOT sanctioned. Same sanction mechanism as the token rule — a
|
|
114
|
+
* dedicated `// phase-id-owner:` comment on the nearest preceding non-blank
|
|
115
|
+
* line — but deliberately WITHOUT its line-level owner-reference escape, so a
|
|
116
|
+
* site that references the owner for one field while re-typing the other is
|
|
117
|
+
* still reported (see BRACKET_CODE_DRIFT_RE's note). Returns [{ line, found }].
|
|
118
|
+
*/
|
|
119
|
+
function findBracketGrammarDrift(text) {
|
|
120
|
+
const out = [];
|
|
121
|
+
const lines = text.split('\n');
|
|
122
|
+
for (let i = 0; i < lines.length; i++) {
|
|
123
|
+
const m = BRACKET_CODE_DRIFT_RE.exec(lines[i]);
|
|
124
|
+
if (!m) continue;
|
|
125
|
+
let j = i - 1;
|
|
126
|
+
while (j >= 0 && lines[j].trim() === '') j--; // nearest preceding non-blank line
|
|
127
|
+
if (j >= 0 && OWNER_RE.test(lines[j])) continue;
|
|
128
|
+
out.push({ line: i + 1, found: m[0] });
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
82
133
|
// Authored TypeScript source only (the generated bin/lib/*.cjs mirror it).
|
|
83
134
|
const SCAN_DIRS = ['src'];
|
|
84
135
|
const SCAN_EXT = new Set(['.cts', '.ts', '.mts']);
|
|
@@ -104,6 +155,67 @@ function walk(dir, acc) {
|
|
|
104
155
|
return acc;
|
|
105
156
|
}
|
|
106
157
|
|
|
158
|
+
// ─── #2761 M4: the heading-baseline selector census ────────────────────────
|
|
159
|
+
//
|
|
160
|
+
// `phaseHeadingPrefixSrcFor(PHASE_HEADING_BASELINE.<MODE>)` is the other half
|
|
161
|
+
// of this seam: it decides which intro grammar a call site compiles, and the
|
|
162
|
+
// MODE argument is a fact about that site's history that no behavioural test
|
|
163
|
+
// can recover — flipping verify's milestone-complete site from LABEL_ONLY to
|
|
164
|
+
// ANY_BRACKET grants a tolerance it has never had, and every behavioural test
|
|
165
|
+
// still passes. Pinning it therefore requires reading the authored source.
|
|
166
|
+
//
|
|
167
|
+
// That reading lives HERE, not in the test suite. `tests/**` runs
|
|
168
|
+
// `local/no-source-grep` at ERROR, and its documented exemption
|
|
169
|
+
// (CONTEXT.md: RULESET.TESTS.no-source-grep.exemption) is reserved for tests
|
|
170
|
+
// whose subject is a runtime CONTRACT FILE — STATE.md, config.toml,
|
|
171
|
+
// hooks.json, agent .md — which `src/*.cts` is not. The suite had claimed that
|
|
172
|
+
// exemption anyway. Scripts are the sanctioned home for source scanning (the
|
|
173
|
+
// rule runs at `warn` in `scripts/**`, and this file already scans src/ for the
|
|
174
|
+
// grammar rules above), so the scan is exported as structured data and the test
|
|
175
|
+
// asserts on the returned census instead of on file text.
|
|
176
|
+
const SELECTOR_CALL_RE = /phaseHeadingPrefixSrcFor\(/g;
|
|
177
|
+
const SELECTOR_BASELINE_RE = /phaseHeadingPrefixSrcFor\(\s*PHASE_HEADING_BASELINE\.(ANY_BRACKET|LABEL_ONLY)/g;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Pure: census the heading-baseline selector calls in `text`.
|
|
181
|
+
*
|
|
182
|
+
* `total` counts EVERY invocation, so a call that does not name a
|
|
183
|
+
* `PHASE_HEADING_BASELINE` member shows up as `total > ANY_BRACKET +
|
|
184
|
+
* LABEL_ONLY` — a hole in the pin rather than a silently uncounted site.
|
|
185
|
+
* Returns { ANY_BRACKET, LABEL_ONLY, total }.
|
|
186
|
+
*/
|
|
187
|
+
function countSelectorBaselines(text) {
|
|
188
|
+
const out = { ANY_BRACKET: 0, LABEL_ONLY: 0, total: 0 };
|
|
189
|
+
for (const m of text.matchAll(SELECTOR_BASELINE_RE)) out[m[1]] += 1;
|
|
190
|
+
out.total = (text.match(SELECTOR_CALL_RE) || []).length;
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Scan the authored source tree and return the selector census keyed by
|
|
196
|
+
* repo-relative path, for every file that consumes the selector at least once.
|
|
197
|
+
* `phase-id.cts` is excluded: it DEFINES the selector, so its own occurrences
|
|
198
|
+
* are the declaration, not a consumer's choice of baseline.
|
|
199
|
+
*/
|
|
200
|
+
function scanSelectorBaselines(root) {
|
|
201
|
+
const census = {};
|
|
202
|
+
for (const dir of SCAN_DIRS) {
|
|
203
|
+
for (const file of walk(path.join(root, dir), [])) {
|
|
204
|
+
const rel = path.relative(root, file);
|
|
205
|
+
if (EXEMPT.has(rel)) continue;
|
|
206
|
+
let text;
|
|
207
|
+
try {
|
|
208
|
+
text = fs.readFileSync(file, 'utf8');
|
|
209
|
+
} catch {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const counts = countSelectorBaselines(text);
|
|
213
|
+
if (counts.total > 0) census[path.basename(file)] = counts;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return census;
|
|
217
|
+
}
|
|
218
|
+
|
|
107
219
|
/**
|
|
108
220
|
* Scan the authored source tree and return every unsanctioned phase-token
|
|
109
221
|
* re-derivation, each annotated with the repo-relative file path.
|
|
@@ -121,7 +233,11 @@ function scanRepo(root) {
|
|
|
121
233
|
continue;
|
|
122
234
|
}
|
|
123
235
|
for (const d of findPhaseIdRegexDrift(text)) {
|
|
124
|
-
violations.push({ file: rel, ...d });
|
|
236
|
+
violations.push({ file: rel, kind: 'token', ...d });
|
|
237
|
+
}
|
|
238
|
+
// #2761 M3: the bracket grammar is the second thing this seam owns.
|
|
239
|
+
for (const d of findBracketGrammarDrift(text)) {
|
|
240
|
+
violations.push({ file: rel, kind: 'bracket', ...d });
|
|
125
241
|
}
|
|
126
242
|
}
|
|
127
243
|
}
|
|
@@ -132,19 +248,28 @@ function main() {
|
|
|
132
248
|
const root = path.join(__dirname, '..');
|
|
133
249
|
const violations = scanRepo(root);
|
|
134
250
|
if (violations.length === 0) {
|
|
135
|
-
process.stdout.write('ok phase-id-drift: no unsanctioned phase-token re-derivations outside phase-id.cts\n');
|
|
251
|
+
process.stdout.write('ok phase-id-drift: no unsanctioned phase-token or bracket-grammar re-derivations outside phase-id.cts\n');
|
|
136
252
|
return;
|
|
137
253
|
}
|
|
138
|
-
process.stderr.write('phase-id-drift: literal re-derivation(s) of
|
|
139
|
-
process.stderr.write(
|
|
140
|
-
process.stderr.write(
|
|
141
|
-
process.stderr.write('
|
|
254
|
+
process.stderr.write('phase-id-drift: literal re-derivation(s) of a canonical grammar found.\n');
|
|
255
|
+
process.stderr.write(`Build the regex from phase-id.cjs \`${CANON_REF}\` (or phaseMarkdownRegexSource for a\n`);
|
|
256
|
+
process.stderr.write(`known number) for the phase-number token, or from ${BRACKET_OWNER_HINT}\n`);
|
|
257
|
+
process.stderr.write('for the bracket grammar — or sanction the site with a dedicated\n');
|
|
258
|
+
process.stderr.write('`// phase-id-owner: <reason>` comment on the line directly above the regex:\n');
|
|
142
259
|
for (const d of violations) {
|
|
143
|
-
process.stderr.write(` ${d.file}:${d.line} ${d.found}\n`);
|
|
260
|
+
process.stderr.write(` [${d.kind}] ${d.file}:${d.line} ${d.found}\n`);
|
|
144
261
|
}
|
|
145
262
|
process.exitCode = 1;
|
|
146
263
|
}
|
|
147
264
|
|
|
148
265
|
if (require.main === module) main();
|
|
149
266
|
|
|
150
|
-
module.exports = {
|
|
267
|
+
module.exports = {
|
|
268
|
+
findPhaseIdRegexDrift,
|
|
269
|
+
findBracketGrammarDrift,
|
|
270
|
+
scanRepo,
|
|
271
|
+
countSelectorBaselines,
|
|
272
|
+
scanSelectorBaselines,
|
|
273
|
+
TOKEN_DRIFT_RE,
|
|
274
|
+
BRACKET_CODE_DRIFT_RE,
|
|
275
|
+
};
|