@opengsd/gsd-core 1.8.0 → 1.9.1
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 +31 -1
- package/agents/gsd-code-fixer.md +107 -34
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +36 -0
- package/agents/gsd-executor.md +20 -7
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +15 -23
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/bin/install.js +236 -107
- package/commands/gsd/plan-review-convergence.md +5 -1
- package/gsd-core/bin/gsd-tools.cjs +882 -4
- package/gsd-core/bin/lib/api-coverage.cjs +22 -8
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/check-command-router.cjs +12 -2
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
- package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
- package/gsd-core/bin/lib/commands.cjs +246 -18
- package/gsd-core/bin/lib/config-loader.cjs +200 -28
- package/gsd-core/bin/lib/config.cjs +90 -5
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +44 -19
- package/gsd-core/bin/lib/install-engine.cjs +1 -0
- package/gsd-core/bin/lib/milestone.cjs +36 -9
- package/gsd-core/bin/lib/model-catalog.cjs +51 -1
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +278 -5
- package/gsd-core/bin/lib/phase.cjs +61 -6
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +1 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/project-root.cjs +48 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
- package/gsd-core/bin/lib/roadmap.cjs +10 -4
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +1 -1
- package/gsd-core/bin/lib/state-document.cjs +164 -20
- package/gsd-core/bin/lib/state-transition.cjs +28 -10
- package/gsd-core/bin/lib/state.cjs +141 -21
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +9 -7
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +32 -0
- package/gsd-core/bin/lib/verification.cjs +51 -14
- package/gsd-core/bin/lib/verify.cjs +146 -22
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -21
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/ai-integration-phase.md +4 -4
- package/gsd-core/workflows/audit-fix.md +4 -0
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +93 -21
- package/gsd-core/workflows/debug.md +10 -2
- package/gsd-core/workflows/diagnose-issues.md +4 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/docs-update.md +8 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase.md +85 -115
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/help/modes/full.md +3 -3
- package/gsd-core/workflows/import.md +4 -1
- package/gsd-core/workflows/ingest-docs.md +4 -0
- package/gsd-core/workflows/map-codebase.md +13 -6
- package/gsd-core/workflows/new-milestone.md +10 -2
- package/gsd-core/workflows/new-project.md +11 -4
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/plan-phase.md +42 -46
- package/gsd-core/workflows/plan-review-convergence.md +18 -14
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +14 -3
- package/gsd-core/workflows/review.md +146 -575
- package/gsd-core/workflows/scan.md +9 -1
- package/gsd-core/workflows/secure-phase.md +10 -2
- package/gsd-core/workflows/ship.md +41 -11
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/ui-phase.md +8 -1
- package/gsd-core/workflows/ui-review.md +8 -1
- package/gsd-core/workflows/update.md +104 -5
- package/gsd-core/workflows/validate-phase.md +10 -2
- package/gsd-core/workflows/verify-work.md +8 -1
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +9 -6
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +9 -6
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +7 -7
- package/pi/gsd.cjs +26 -1
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +24 -0
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +6 -4
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/gen-registry.cjs +39 -15
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +372 -94
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/validate-registry.cjs +10 -6
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* gen-emitted-baseline.cjs — publishes the emitted-baseline artifact the
|
|
6
|
+
* differential attribution check reads via `resolveBaseline()` (#2724, ADR-2719 §5).
|
|
7
|
+
*
|
|
8
|
+
* ## Why this exists
|
|
9
|
+
*
|
|
10
|
+
* Phase 3 (#2723) built `resolveBaseline()` (tests/helpers/emitted-baseline.cjs) and its
|
|
11
|
+
* cache-key staleness discipline, but nothing produced the artifact it reads — the
|
|
12
|
+
* dual-run window instead read the baseline straight off the committed golden fixtures
|
|
13
|
+
* at the base ref (`baselineManifestsAtRef`). Phase 4 deletes those fixtures, so
|
|
14
|
+
* something has to build and publish `{version, sha, manifests, sizes}` for real.
|
|
15
|
+
*
|
|
16
|
+
* ## What it does
|
|
17
|
+
*
|
|
18
|
+
* Builds the emitted manifest set (19 real installer spawns) and the workflow/agent size
|
|
19
|
+
* maps for the tree at `--dir <path>` (default: THIS script's own checkout — i.e.
|
|
20
|
+
* whatever commit is currently checked out where `gen-emitted-baseline.cjs` itself
|
|
21
|
+
* lives). Writes the result to `--out <path>` (default `.gsd-cache/emitted-baseline.json`).
|
|
22
|
+
*
|
|
23
|
+
* `--dir` decouples "which copy of this script runs" from "which tree gets measured"
|
|
24
|
+
* (#2767). The MEASUREMENT SCHEMA — this script, and the `currentManifests`/
|
|
25
|
+
* `currentSizes`/`buildParityManifest` functions it calls — always comes from wherever
|
|
26
|
+
* `gen-emitted-baseline.cjs` itself is being run from (relative `require`s resolve
|
|
27
|
+
* there); only the tree being measured (which `bin/install.js` gets spawned, which
|
|
28
|
+
* `hooks/`/`gsd-core/workflows/`/`agents/` get read) moves to `--dir`. That is what lets
|
|
29
|
+
* a differential apply ONE definition of "the emitted manifest" to two different
|
|
30
|
+
* commits and stay comparable even as that definition evolves — see
|
|
31
|
+
* `tests/helpers/emitted-runtime.cjs`'s `buildBaselineAtRef` for the caller that
|
|
32
|
+
* exercises this for real, and why it must NOT run the `--dir` tree's own copy of this
|
|
33
|
+
* script (which may be an older version, or — for any base ref that predates the PR
|
|
34
|
+
* that added this script at all — may not exist there yet).
|
|
35
|
+
*
|
|
36
|
+
* ## Callers
|
|
37
|
+
*
|
|
38
|
+
* 1. CI's push-to-`next` job runs this straight after `next` advances (no `--dir`,
|
|
39
|
+
* measuring its own checkout), then uploads `.gsd-cache/emitted-baseline.json` as a
|
|
40
|
+
* cache entry keyed on the merge sha (.github/workflows/test.yml,
|
|
41
|
+
* `publish-emitted-baseline` job).
|
|
42
|
+
* 2. `tests/emitted-attribution.test.cjs`'s real-tree test passes
|
|
43
|
+
* `buildBaselineAtRef` (tests/helpers/emitted-runtime.cjs) to `resolveBaseline()`
|
|
44
|
+
* as the `buildFallback` for a cache miss: it checks out `base` into a throwaway
|
|
45
|
+
* `git worktree`, symlinks in `node_modules` and runs `npm run build:lib` there
|
|
46
|
+
* (this script and the test helpers are Node-builtins-only per CONTRIBUTING.md's
|
|
47
|
+
* "No external dependencies in core", but `tests/helpers/install-shared.cjs`
|
|
48
|
+
* requires the TSC-compiled, gitignored `gsd-core/bin/lib/*.cjs`, so that one build
|
|
49
|
+
* step is unavoidable), then spawns THIS repo's OWN `gen-emitted-baseline.cjs`
|
|
50
|
+
* with `--dir <worktree> --out <tmp>` and reads the artifact back.
|
|
51
|
+
*
|
|
52
|
+
* Every git subprocess is bounded (CLAUDE.md → KNOWN DEFECTS: unbounded subprocesses
|
|
53
|
+
* hang CI silently).
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
const fs = require('node:fs');
|
|
57
|
+
const path = require('node:path');
|
|
58
|
+
const { execFileSync } = require('node:child_process');
|
|
59
|
+
|
|
60
|
+
const { runMain, ExitError } = require('./lib/cli-exit.cjs');
|
|
61
|
+
const { BUILD_SCRIPT } = require('../tests/helpers/install-shared.cjs');
|
|
62
|
+
const { currentManifests, currentSizes, git } = require('../tests/helpers/emitted-runtime.cjs');
|
|
63
|
+
const { BASELINE_VERSION } = require('../tests/helpers/emitted-baseline.cjs');
|
|
64
|
+
|
|
65
|
+
const REPO_ROOT = path.join(__dirname, '..');
|
|
66
|
+
const DEFAULT_OUT = path.join(REPO_ROOT, '.gsd-cache', 'emitted-baseline.json');
|
|
67
|
+
|
|
68
|
+
// Reuses emitted-runtime.cjs's `git()` (not a local execFileSync) so the
|
|
69
|
+
// `-c safe.directory=<dir>` fix for the remote runner's dubious-ownership
|
|
70
|
+
// mount (#2767) has exactly one source of truth — see that module's
|
|
71
|
+
// `safeDirArgs` doc comment.
|
|
72
|
+
function resolveHeadSha(cwd) {
|
|
73
|
+
return git(['rev-parse', 'HEAD'], { cwd }).trim();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseArgs(argv) {
|
|
77
|
+
let out = DEFAULT_OUT;
|
|
78
|
+
let dir = REPO_ROOT;
|
|
79
|
+
for (let i = 0; i < argv.length; i++) {
|
|
80
|
+
if (argv[i] === '--out') {
|
|
81
|
+
out = argv[i + 1];
|
|
82
|
+
i++;
|
|
83
|
+
} else if (argv[i] === '--dir') {
|
|
84
|
+
dir = argv[i + 1];
|
|
85
|
+
i++;
|
|
86
|
+
} else {
|
|
87
|
+
throw new ExitError(2, `gen-emitted-baseline: unknown argument "${argv[i]}"`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!out || typeof out !== 'string') {
|
|
91
|
+
throw new ExitError(2, 'gen-emitted-baseline: --out requires a path');
|
|
92
|
+
}
|
|
93
|
+
if (!dir || typeof dir !== 'string') {
|
|
94
|
+
throw new ExitError(2, 'gen-emitted-baseline: --dir requires a path');
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
out: path.isAbsolute(out) ? out : path.join(process.cwd(), out),
|
|
98
|
+
dir: path.isAbsolute(dir) ? dir : path.join(process.cwd(), dir),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function main() {
|
|
103
|
+
const { out, dir } = parseArgs(process.argv.slice(2));
|
|
104
|
+
|
|
105
|
+
if (!fs.existsSync(dir)) {
|
|
106
|
+
throw new ExitError(1, `gen-emitted-baseline: --dir "${dir}" does not exist`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const sha = resolveHeadSha(dir);
|
|
110
|
+
if (!/^[0-9a-f]{40}$/.test(sha)) {
|
|
111
|
+
throw new ExitError(1, `gen-emitted-baseline: HEAD of "${dir}" did not resolve to a 40-hex sha: ${sha}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// hooks/dist is gitignored and built; a scoped CI checkout may not have run
|
|
115
|
+
// build:hooks yet. Idempotent, mirrors the real-tree test. Built INSIDE `dir` — the
|
|
116
|
+
// MEASURED tree's own hooks/ source, not this script's — so a `--dir` pointed at a
|
|
117
|
+
// different checkout (e.g. a base-ref worktree) reflects that checkout's hooks, not
|
|
118
|
+
// the caller's. `dir === REPO_ROOT` (the default, no `--dir`) collapses to the
|
|
119
|
+
// pre-#2767 behavior of building this own repo's hooks/dist.
|
|
120
|
+
const buildScript = dir === REPO_ROOT ? BUILD_SCRIPT : path.join(dir, 'scripts', 'build-hooks.js');
|
|
121
|
+
execFileSync(process.execPath, [buildScript], { cwd: dir, encoding: 'utf-8', stdio: 'pipe', timeout: 120_000 });
|
|
122
|
+
|
|
123
|
+
// The measurement SCHEMA (currentManifests/currentSizes/buildParityManifest) is
|
|
124
|
+
// always THIS script's own — only the tree being measured moves with `repoRoot`.
|
|
125
|
+
const manifests = currentManifests({ repoRoot: dir });
|
|
126
|
+
const sizes = currentSizes({ repoRoot: dir });
|
|
127
|
+
|
|
128
|
+
const artifact = { version: BASELINE_VERSION, sha, manifests, sizes };
|
|
129
|
+
|
|
130
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
131
|
+
fs.writeFileSync(out, `${JSON.stringify(artifact, null, 2)}\n`, 'utf8');
|
|
132
|
+
|
|
133
|
+
const familyCount = Object.keys(manifests).length;
|
|
134
|
+
const sizeCount = Object.keys(sizes).length;
|
|
135
|
+
process.stdout.write(
|
|
136
|
+
`gen-emitted-baseline: wrote ${out} (dir=${dir}, sha=${sha.slice(0, 12)}, ${familyCount} families, ${sizeCount} sized files)\n`,
|
|
137
|
+
);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (require.main === module) {
|
|
142
|
+
runMain(main);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = { parseArgs, resolveHeadSha, DEFAULT_OUT, REPO_ROOT };
|
package/scripts/gen-registry.cjs
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* scripts/gen-registry.cjs — generates docs/registries/capability-registry.md
|
|
6
|
-
*
|
|
7
|
-
* from
|
|
8
|
-
* Issue #2182.
|
|
5
|
+
* scripts/gen-registry.cjs — generates docs/registries/capability-registry.md,
|
|
6
|
+
* docs/registries/eos-registry.md, and docs/registries/reviewer-registry.md
|
|
7
|
+
* from their corresponding source JSON, via registry-schema.cjs#renderMarkdown.
|
|
8
|
+
* Issue #2182 (capability/eos); issue #2904 (reviewer).
|
|
9
|
+
*
|
|
10
|
+
* eos.json and reviewers.json are both OPTIONAL sources (`SOURCES[].optional`)
|
|
11
|
+
* — an absent one is skipped silently. capabilities.json is the primary
|
|
12
|
+
* source and is never optional.
|
|
9
13
|
*
|
|
10
14
|
* NOT to be confused with `scripts/gen-capability-registry.cjs`: that script
|
|
11
15
|
* generates the RUNTIME capability manifest consumed by the host at runtime
|
|
@@ -34,7 +38,8 @@ const { renderMarkdown } = require('./registry-schema.cjs');
|
|
|
34
38
|
|
|
35
39
|
const SOURCES = [
|
|
36
40
|
{ type: 'capability', jsonFile: 'capabilities.json', mdFile: 'capability-registry.md' },
|
|
37
|
-
{ type: 'eos', jsonFile: 'eos.json', mdFile: 'eos-registry.md' },
|
|
41
|
+
{ type: 'eos', jsonFile: 'eos.json', mdFile: 'eos-registry.md', optional: true },
|
|
42
|
+
{ type: 'reviewer', jsonFile: 'reviewers.json', mdFile: 'reviewer-registry.md', optional: true },
|
|
38
43
|
];
|
|
39
44
|
|
|
40
45
|
/**
|
|
@@ -57,14 +62,16 @@ function getRegistriesDir() {
|
|
|
57
62
|
* Render the markdown for a single registry type from its committed source
|
|
58
63
|
* JSON.
|
|
59
64
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* `capabilities.json` is
|
|
64
|
-
*
|
|
65
|
-
* (
|
|
65
|
+
* Optionality is a per-source data flag (`SOURCES[].optional`), not a
|
|
66
|
+
* hardcoded type literal: `eos.json` (pre-PR2) and `reviewers.json` (issue
|
|
67
|
+
* #2904) are both optional — an absent source JSON returns null and callers
|
|
68
|
+
* treat that as "nothing to do". `capabilities.json` is still the primary
|
|
69
|
+
* registry source and is never optional: a missing `capabilities.json` is
|
|
70
|
+
* ALWAYS an error (never a silent "up to date" pass), mirroring the same
|
|
71
|
+
* `optional` flag in `scripts/validate-registry.cjs`
|
|
72
|
+
* (`optional && !exists → continue`).
|
|
66
73
|
*
|
|
67
|
-
* @param {'capability'|'eos'} type
|
|
74
|
+
* @param {'capability'|'eos'|'reviewer'} type
|
|
68
75
|
* @returns {string|null}
|
|
69
76
|
*/
|
|
70
77
|
function renderFor(type) {
|
|
@@ -73,14 +80,31 @@ function renderFor(type) {
|
|
|
73
80
|
|
|
74
81
|
const jsonPath = path.join(getRegistriesDir(), source.jsonFile);
|
|
75
82
|
if (!fs.existsSync(jsonPath)) {
|
|
76
|
-
if (
|
|
83
|
+
if (source.optional) return null;
|
|
77
84
|
throw new ExitError(
|
|
78
85
|
1,
|
|
79
86
|
`${source.jsonFile} does not exist at ${jsonPath}. Run:\n node scripts/gen-registry.cjs --write\n(after adding docs/registries/${source.jsonFile})`,
|
|
80
87
|
);
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
|
|
90
|
+
// A malformed source JSON must surface as an actionable CLI error, not an
|
|
91
|
+
// unhandled SyntaxError with a raw Node stack trace — mirrors
|
|
92
|
+
// scripts/validate-registry.cjs#validateFile's try/catch around JSON.parse.
|
|
93
|
+
let entries;
|
|
94
|
+
try {
|
|
95
|
+
entries = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
96
|
+
} catch (err) {
|
|
97
|
+
throw new ExitError(1, `${source.jsonFile} is not valid JSON at ${jsonPath}: ${err.message}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Mirrors validate-registry.cjs's explicit non-array rejection: a source
|
|
101
|
+
// JSON that parses to a non-array (object, string, etc.) would otherwise
|
|
102
|
+
// throw an opaque TypeError from `[...entries].sort()` in renderMarkdown,
|
|
103
|
+
// or silently mis-render for an iterable-but-wrong-shape value like a string.
|
|
104
|
+
if (!Array.isArray(entries)) {
|
|
105
|
+
throw new ExitError(1, `${source.jsonFile} must be a JSON array of entries`);
|
|
106
|
+
}
|
|
107
|
+
|
|
84
108
|
return renderMarkdown(entries, { type, sourceFile: source.jsonFile });
|
|
85
109
|
}
|
|
86
110
|
|
|
@@ -91,7 +115,7 @@ function main() {
|
|
|
91
115
|
|
|
92
116
|
for (const { type, mdFile } of SOURCES) {
|
|
93
117
|
const rendered = renderFor(type);
|
|
94
|
-
if (rendered === null) continue; // source JSON absent (eos.json before PR2)
|
|
118
|
+
if (rendered === null) continue; // source JSON absent and optional (eos.json before PR2 / reviewers.json)
|
|
95
119
|
|
|
96
120
|
const mdPath = path.join(registriesDir, mdFile);
|
|
97
121
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lint-compiled-artifact-sync — fail when a *tracked* compiled artifact under
|
|
4
|
+
* gsd-core/bin/lib/ has drifted from its src/*.cts source.
|
|
5
|
+
*
|
|
6
|
+
* ADR-457 compiles src/*.cts to gsd-core/bin/lib/*.cjs at build time and expects
|
|
7
|
+
* those artifacts to be gitignored. Most are (see .gitignore). A handful are
|
|
8
|
+
* still tracked because the migration that moved the module into src/ did not
|
|
9
|
+
* also add the emitted .cjs to .gitignore. While a compiled artifact remains
|
|
10
|
+
* tracked, the committed bytes are what ships to anyone who reads the repo
|
|
11
|
+
* without building — so they must match the source.
|
|
12
|
+
*
|
|
13
|
+
* #2653: gsd-core/bin/lib/api-coverage.cjs sat four days behind its .cts after
|
|
14
|
+
* PR #2551 changed the source without regenerating the artifact, shipping a
|
|
15
|
+
* module that silently lacked the entire #2366 fix while CI stayed green.
|
|
16
|
+
*
|
|
17
|
+
* This check is deliberately regime-agnostic: it asserts a property of whatever
|
|
18
|
+
* is tracked right now. If the remaining artifacts are later untracked and
|
|
19
|
+
* gitignored (the ADR-457 end state), the tracked set becomes empty and this
|
|
20
|
+
* script passes trivially — no edit required.
|
|
21
|
+
*
|
|
22
|
+
* Usage: node scripts/lint-compiled-artifact-sync.cjs [--check]
|
|
23
|
+
* Exit 0 when every tracked artifact matches a fresh compile; 1 otherwise.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
const { execFileSync } = require('node:child_process');
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const os = require('node:os');
|
|
31
|
+
const path = require('node:path');
|
|
32
|
+
|
|
33
|
+
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
34
|
+
const LIB_DIR = path.join('gsd-core', 'bin', 'lib');
|
|
35
|
+
const SRC_DIR = 'src';
|
|
36
|
+
|
|
37
|
+
/** Frozen reason codes so tests assert on structure, not prose. */
|
|
38
|
+
const REASON = Object.freeze({
|
|
39
|
+
OK: 'ok_artifacts_in_sync',
|
|
40
|
+
DRIFTED: 'fail_artifact_drifted',
|
|
41
|
+
BUILD_FAILED: 'fail_build_failed',
|
|
42
|
+
MISSING_EMIT: 'fail_missing_emit',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function git(args) {
|
|
46
|
+
return execFileSync('git', args, { cwd: REPO_ROOT, encoding: 'utf8' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Tracked .cjs files under gsd-core/bin/lib/ that have a matching src/*.cts.
|
|
51
|
+
* Uses `git ls-files` so the set is derived from what git actually tracks
|
|
52
|
+
* rather than a hand-maintained list that could itself drift.
|
|
53
|
+
*/
|
|
54
|
+
function trackedCompiledArtifacts() {
|
|
55
|
+
const tracked = git(['ls-files', LIB_DIR]).split('\n').filter((l) => l.endsWith('.cjs'));
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const rel of tracked) {
|
|
58
|
+
const stem = path.basename(rel, '.cjs');
|
|
59
|
+
const subdir = path.dirname(path.relative(LIB_DIR, rel));
|
|
60
|
+
const srcRel = path.join(SRC_DIR, subdir === '.' ? '' : subdir, `${stem}.cts`);
|
|
61
|
+
if (fs.existsSync(path.join(REPO_ROOT, srcRel))) out.push({ artifact: rel, source: srcRel });
|
|
62
|
+
}
|
|
63
|
+
return out.sort((a, b) => (a.artifact < b.artifact ? -1 : a.artifact > b.artifact ? 1 : 0));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Compile the whole project to a throwaway outDir so the work tree is untouched. */
|
|
67
|
+
function compileToTemp() {
|
|
68
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-artifact-sync-'));
|
|
69
|
+
try {
|
|
70
|
+
execFileSync(
|
|
71
|
+
process.execPath,
|
|
72
|
+
[
|
|
73
|
+
path.join(REPO_ROOT, 'node_modules', 'typescript', 'bin', 'tsc'),
|
|
74
|
+
'-p', path.join(REPO_ROOT, 'tsconfig.build.json'),
|
|
75
|
+
'--outDir', tmp,
|
|
76
|
+
// A throwaway outDir must not reuse the in-tree incremental state, or
|
|
77
|
+
// tsc skips emit for files it believes are already current.
|
|
78
|
+
'--incremental', 'false',
|
|
79
|
+
'--tsBuildInfoFile', 'null',
|
|
80
|
+
],
|
|
81
|
+
{ cwd: REPO_ROOT, encoding: 'utf8', stdio: 'pipe' },
|
|
82
|
+
);
|
|
83
|
+
return { ok: true, dir: tmp };
|
|
84
|
+
} catch (err) {
|
|
85
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
86
|
+
const detail = [err.stdout, err.stderr].filter(Boolean).join('\n').trim();
|
|
87
|
+
return { ok: false, detail };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function main() {
|
|
92
|
+
const pairs = trackedCompiledArtifacts();
|
|
93
|
+
if (pairs.length === 0) {
|
|
94
|
+
console.log('ok compiled-artifact-sync: no tracked compiled artifacts (ADR-457 end state)');
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const build = compileToTemp();
|
|
99
|
+
if (!build.ok) {
|
|
100
|
+
console.error(`FAIL compiled-artifact-sync: ${REASON.BUILD_FAILED}`);
|
|
101
|
+
console.error(build.detail);
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const drifted = [];
|
|
106
|
+
const missing = [];
|
|
107
|
+
try {
|
|
108
|
+
for (const { artifact, source } of pairs) {
|
|
109
|
+
const fresh = path.join(build.dir, path.relative(LIB_DIR, artifact));
|
|
110
|
+
if (!fs.existsSync(fresh)) { missing.push({ artifact, source }); continue; }
|
|
111
|
+
const a = fs.readFileSync(path.join(REPO_ROOT, artifact));
|
|
112
|
+
const b = fs.readFileSync(fresh);
|
|
113
|
+
if (!a.equals(b)) drifted.push({ artifact, source, committed: a.length, expected: b.length });
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
fs.rmSync(build.dir, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (missing.length > 0) {
|
|
120
|
+
console.error(`FAIL compiled-artifact-sync: ${REASON.MISSING_EMIT}`);
|
|
121
|
+
for (const m of missing) console.error(` ${m.artifact} — no emit produced from ${m.source}`);
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (drifted.length > 0) {
|
|
126
|
+
console.error(`FAIL compiled-artifact-sync: ${REASON.DRIFTED}`);
|
|
127
|
+
for (const d of drifted) {
|
|
128
|
+
console.error(` ${d.artifact} (${d.committed} bytes) != compile of ${d.source} (${d.expected} bytes)`);
|
|
129
|
+
}
|
|
130
|
+
console.error('');
|
|
131
|
+
console.error('The committed artifact is what ships to anyone reading the repo without');
|
|
132
|
+
console.error('building, so it must match its source. Fix with:');
|
|
133
|
+
console.error(' npm run build:lib && git add ' + drifted.map((d) => d.artifact).join(' '));
|
|
134
|
+
console.error('');
|
|
135
|
+
console.error('Alternatively, per ADR-457 these artifacts are meant to be gitignored —');
|
|
136
|
+
console.error('untracking them (git rm --cached + .gitignore) also resolves this.');
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(`ok compiled-artifact-sync: ${pairs.length} tracked artifact(s) match their source`);
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (require.main === module) process.exitCode = main();
|
|
145
|
+
|
|
146
|
+
module.exports = { REASON, trackedCompiledArtifacts };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* lint-emitted-drift-ack — refuse to merge a broken emitted-drift acknowledgment (#2789).
|
|
6
|
+
*
|
|
7
|
+
* The ack document is read from TWO sides: the working tree (`readAckFile`) and the BASE
|
|
8
|
+
* REF (`readAckFileAtRef`), because an entry already present at the base is spent and may
|
|
9
|
+
* no longer clear a delta. The base-side read fails LOUDLY on a document it cannot parse
|
|
10
|
+
* — it has to, since silently inheriting nothing would leave every entry able to consume
|
|
11
|
+
* a delta, which is the pre-#2789 gate.
|
|
12
|
+
*
|
|
13
|
+
* That makes a corrupt document ON THE BASE BRANCH unusually expensive: it reds every PR
|
|
14
|
+
* that carries an ack until someone repairs it. The real-tree test avoids an outright
|
|
15
|
+
* deadlock (a tree with no ack never consults the base, so the repair PR still lands),
|
|
16
|
+
* but the cheaper answer is to never let a broken document reach the base at all. This
|
|
17
|
+
* runs in `lint:ci`, so a PR carrying one cannot go green and cannot merge.
|
|
18
|
+
*
|
|
19
|
+
* This validator is DELIBERATELY STANDALONE. `scripts/` ships in the npm package and
|
|
20
|
+
* `tests/` does not (package.json `files`), so requiring the gate's own `parseAck` from
|
|
21
|
+
* here would be a MODULE_NOT_FOUND in the published package. The duplication is bounded
|
|
22
|
+
* by a parity test — `tests/emitted-attribution.test.cjs` runs both surfaces over one
|
|
23
|
+
* corpus and fails if they ever disagree about what is schema-valid.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
|
|
29
|
+
const ACK_VERSION = 1;
|
|
30
|
+
const ACK_REPO_PATH = 'tests/emitted-drift-ack.json';
|
|
31
|
+
const REPO_ROOT = path.join(__dirname, '..');
|
|
32
|
+
|
|
33
|
+
const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate an ack document's raw text.
|
|
37
|
+
*
|
|
38
|
+
* `schemaErrors` are the ones that must agree with the gate's `parseAck` — the shape
|
|
39
|
+
* contract. `policyErrors` are lint-only rules that `parseAck` deliberately does NOT
|
|
40
|
+
* enforce, because they are about what may be COMMITTED rather than what may be parsed:
|
|
41
|
+
* a present-but-entryless document parses fine and signals nothing, so it must be deleted
|
|
42
|
+
* rather than left behind.
|
|
43
|
+
*
|
|
44
|
+
* @param {string|null} raw file contents, or null when the file is absent
|
|
45
|
+
* @returns {{ schemaErrors: string[], policyErrors: string[], ok: boolean }}
|
|
46
|
+
*/
|
|
47
|
+
function validateAckText(raw) {
|
|
48
|
+
const schemaErrors = [];
|
|
49
|
+
const policyErrors = [];
|
|
50
|
+
const done = () => ({ schemaErrors, policyErrors, ok: schemaErrors.length === 0 && policyErrors.length === 0 });
|
|
51
|
+
|
|
52
|
+
if (raw === null) return done(); // absent is the healthy steady state
|
|
53
|
+
|
|
54
|
+
if (raw.trim() === '') {
|
|
55
|
+
schemaErrors.push(`${ACK_REPO_PATH} is present but empty`);
|
|
56
|
+
return done();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let doc;
|
|
60
|
+
try {
|
|
61
|
+
doc = JSON.parse(raw);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
schemaErrors.push(`${ACK_REPO_PATH} is not valid JSON: ${err.message}`);
|
|
64
|
+
return done();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A document that is literally `null` is POLICY, not schema. The gate's `parseAck` uses
|
|
68
|
+
// `null` as its "absent == no acks" sentinel, so it reads such a file as legal and
|
|
69
|
+
// harmless — and the parity test holds us to that. It is still not something to commit:
|
|
70
|
+
// it declares nothing, so the remedy is the same as an entryless document.
|
|
71
|
+
if (doc === null) {
|
|
72
|
+
policyErrors.push(
|
|
73
|
+
`${ACK_REPO_PATH} contains "null" and declares no acknowledgments. Delete the file — `
|
|
74
|
+
+ 'the healthy steady state is no file at all.',
|
|
75
|
+
);
|
|
76
|
+
return done();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!isPlainObject(doc)) {
|
|
80
|
+
schemaErrors.push(
|
|
81
|
+
`${ACK_REPO_PATH}: must be a JSON object, got ${Array.isArray(doc) ? 'array' : typeof doc}`,
|
|
82
|
+
);
|
|
83
|
+
return done();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (doc.version !== undefined && doc.version !== ACK_VERSION) {
|
|
87
|
+
schemaErrors.push(
|
|
88
|
+
`${ACK_REPO_PATH}: unsupported version ${JSON.stringify(doc.version)} (expected ${ACK_VERSION})`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const paths = doc.paths;
|
|
93
|
+
if (paths !== undefined && !isPlainObject(paths)) {
|
|
94
|
+
schemaErrors.push(`${ACK_REPO_PATH}: "paths" must be an object of <emitted path> -> { reason }`);
|
|
95
|
+
return done();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const entries = paths === undefined ? [] : Object.entries(paths);
|
|
99
|
+
for (const [rel, value] of entries) {
|
|
100
|
+
const reason = isPlainObject(value) ? value.reason : value;
|
|
101
|
+
if (typeof reason !== 'string' || reason.trim() === '') {
|
|
102
|
+
schemaErrors.push(`${ACK_REPO_PATH}: ack for "${rel}" has no non-empty "reason"`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Lint-only. An entryless document parses cleanly and acknowledges nothing, so it is
|
|
107
|
+
// pure confusion on the base branch — and it is exactly what a contributor leaves
|
|
108
|
+
// behind after removing the last entry by hand.
|
|
109
|
+
if (entries.length === 0) {
|
|
110
|
+
policyErrors.push(
|
|
111
|
+
`${ACK_REPO_PATH} is present but declares no acknowledgments. Delete the file — an `
|
|
112
|
+
+ 'empty one signals nothing, and the healthy steady state is no file at all.',
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return done();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readIfPresent(file) {
|
|
120
|
+
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function main() {
|
|
124
|
+
const file = path.join(REPO_ROOT, ...ACK_REPO_PATH.split('/'));
|
|
125
|
+
const result = validateAckText(readIfPresent(file));
|
|
126
|
+
const all = [...result.schemaErrors, ...result.policyErrors];
|
|
127
|
+
|
|
128
|
+
if (all.length) {
|
|
129
|
+
console.error(`lint-emitted-drift-ack: ${all.length} problem(s) in ${ACK_REPO_PATH}\n`);
|
|
130
|
+
for (const e of all) console.error(` - ${e}`);
|
|
131
|
+
console.error(
|
|
132
|
+
'\nThis blocks the merge on purpose. The base-side reader fails loudly on a document '
|
|
133
|
+
+ 'it cannot parse, so a broken one on the base branch reds every PR that carries an '
|
|
134
|
+
+ 'acknowledgment. Fix or delete the file here, where it is cheap.',
|
|
135
|
+
);
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(
|
|
141
|
+
fs.existsSync(file)
|
|
142
|
+
? `ok lint-emitted-drift-ack: ${ACK_REPO_PATH} is well-formed`
|
|
143
|
+
: `ok lint-emitted-drift-ack: ${ACK_REPO_PATH} absent (the healthy steady state)`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (require.main === module) main();
|
|
148
|
+
|
|
149
|
+
module.exports = { validateAckText, ACK_VERSION, ACK_REPO_PATH };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* lint-fix-has-regression-test.cjs — gate: every fix(#NNNN) commit must
|
|
6
|
+
* include at least one behavioral test file (tests/*.test.cjs) that is NOT
|
|
7
|
+
* an auto-generated fixture/baseline.
|
|
8
|
+
*
|
|
9
|
+
* ## Why
|
|
10
|
+
*
|
|
11
|
+
* CONTRIBUTING.md:47: "Fix it. Write a test that would have caught the bug."
|
|
12
|
+
* CLAUDE.md: "Regression Protocol: Write the regression test first."
|
|
13
|
+
*
|
|
14
|
+
* Three merged PRs shipped without regression tests because the golden-fixture
|
|
15
|
+
* regeneration (20+ files under tests/) camouflaged the absence of real
|
|
16
|
+
* *.test.cjs changes. This gate makes the rule machine-enforced.
|
|
17
|
+
*
|
|
18
|
+
* ## What this enforces
|
|
19
|
+
*
|
|
20
|
+
* For every commit in `git log origin/next..HEAD` whose subject matches
|
|
21
|
+
* /^fix\(#\d+\)/ or /^feat\(#\d+\)/, the cumulative diff must include at
|
|
22
|
+
* least one file under tests/ matching /\.test\.cjs$/ that is NOT:
|
|
23
|
+
* - under tests/fixtures/
|
|
24
|
+
* - under tests/install-tree/
|
|
25
|
+
* - a *-baseline.json file
|
|
26
|
+
*
|
|
27
|
+
* If the filtered list is empty, the gate fails with a message naming the
|
|
28
|
+
* fix commits and directing the author to add a regression test.
|
|
29
|
+
*
|
|
30
|
+
* ## Overrides
|
|
31
|
+
*
|
|
32
|
+
* Set GSD_SKIP_REGRESSION_TEST_GATE=1 for legitimate exceptions (e.g. a
|
|
33
|
+
* pure-config or pure-refactor PR where no behavioral test is possible).
|
|
34
|
+
* This env var is auditable in CI logs.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const { execSync } = require('child_process');
|
|
38
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
39
|
+
|
|
40
|
+
const FIX_OR_FEAT_RE = /^(?:fix|feat)\(#(\d+)\)/;
|
|
41
|
+
|
|
42
|
+
const EXCLUDE_PATTERNS = [
|
|
43
|
+
/tests[\\/]fixtures[\\/]/,
|
|
44
|
+
/tests[\\/]install-tree[\\/]/,
|
|
45
|
+
/-baseline\.json$/,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function isRealTestFile(filePath) {
|
|
49
|
+
if (!filePath.includes('tests/')) return false;
|
|
50
|
+
if (!/\.test\.cjs$/.test(filePath)) return false;
|
|
51
|
+
return !EXCLUDE_PATTERNS.some((re) => re.test(filePath));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getFixCommits(baseRef) {
|
|
55
|
+
const log = execSync(
|
|
56
|
+
`git log ${baseRef}..HEAD --format='%H%x09%s' --no-merges`,
|
|
57
|
+
{ encoding: 'utf8', timeout: 10000 },
|
|
58
|
+
);
|
|
59
|
+
return log
|
|
60
|
+
.trim()
|
|
61
|
+
.split('\n')
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map((line) => {
|
|
64
|
+
const [sha, subject] = line.split('\t');
|
|
65
|
+
const match = subject.match(FIX_OR_FEAT_RE);
|
|
66
|
+
return match ? { sha: sha.slice(0, 12), subject, issue: match[1] } : null;
|
|
67
|
+
})
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getChangedTestFiles(baseRef) {
|
|
72
|
+
const diff = execSync(
|
|
73
|
+
`git diff ${baseRef}...HEAD --name-only --no-merges`,
|
|
74
|
+
{ encoding: 'utf8', timeout: 10000 },
|
|
75
|
+
);
|
|
76
|
+
return diff
|
|
77
|
+
.trim()
|
|
78
|
+
.split('\n')
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.filter(isRealTestFile);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function main() {
|
|
84
|
+
if (process.env.GSD_SKIP_REGRESSION_TEST_GATE === '1') {
|
|
85
|
+
console.log('lint-fix-has-regression-test: SKIPPED (GSD_SKIP_REGRESSION_TEST_GATE=1)');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const baseRef = process.env.GSD_REGRESSION_GATE_BASE || 'origin/next';
|
|
90
|
+
|
|
91
|
+
let fixCommits;
|
|
92
|
+
try {
|
|
93
|
+
fixCommits = getFixCommits(baseRef);
|
|
94
|
+
} catch {
|
|
95
|
+
console.log(`lint-fix-has-regression-test: no fix/feat commits found vs ${baseRef}, skipping`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (fixCommits.length === 0) {
|
|
100
|
+
console.log('lint-fix-has-regression-test: no fix/feat commits, passing');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let testFiles;
|
|
105
|
+
try {
|
|
106
|
+
testFiles = getChangedTestFiles(baseRef);
|
|
107
|
+
} catch {
|
|
108
|
+
testFiles = [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (testFiles.length === 0) {
|
|
112
|
+
const commitList = fixCommits
|
|
113
|
+
.map((c) => ` ${c.sha} ${c.subject}`)
|
|
114
|
+
.join('\n');
|
|
115
|
+
throw new ExitError(1,
|
|
116
|
+
`lint-fix-has-regression-test: ${fixCommits.length} fix/feat commit(s) but ZERO behavioral test files (*.test.cjs) in the diff.\n` +
|
|
117
|
+
`Auto-generated fixtures (tests/fixtures/, *-baseline.json) do NOT count.\n\n` +
|
|
118
|
+
`Fix commits:\n${commitList}\n\n` +
|
|
119
|
+
`CONTRIBUTING.md:47: "Write a test that would have caught the bug."\n` +
|
|
120
|
+
`Add a regression test to an existing tests/*.test.cjs file, or set ` +
|
|
121
|
+
`GSD_SKIP_REGRESSION_TEST_GATE=1 if no behavioral test is possible (auditable in CI).`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(
|
|
126
|
+
`lint-fix-has-regression-test: PASS — ${fixCommits.length} fix/feat commit(s), ` +
|
|
127
|
+
`${testFiles.length} behavioral test file(s): ${testFiles.join(', ')}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
runMain(main);
|
|
@@ -56,6 +56,15 @@ const REGISTRY = [
|
|
|
56
56
|
sourceFile: 'src/init.cts',
|
|
57
57
|
testFile: 'tests/agent-skills.test.cjs',
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
// Registered by #1880 (ADR-1411 amendment "corrupt is not absent"). Until
|
|
61
|
+
// this entry existed the config-loader seam carried the provenance contract
|
|
62
|
+
// with nothing guarding it, so a regression that collapsed configured_empty
|
|
63
|
+
// back into not_configured would have shipped silently.
|
|
64
|
+
verb: 'config-loader',
|
|
65
|
+
sourceFile: 'src/config-loader.cts',
|
|
66
|
+
testFile: 'tests/config-loader.test.cjs',
|
|
67
|
+
},
|
|
59
68
|
];
|
|
60
69
|
|
|
61
70
|
// Markers that MUST appear in every registered verb's test file.
|