scripts/spec-baseline-check.mjs
Rama master · réplica de lectura del repositorio original.
#!/usr/bin/env node
// Measure selected mutation baselines from structured runner events, never display text.
// Approved Windows skips establish count compatibility only, not executed assertions.
// --all measures every spec (the RELEASE gate); the default is change-scoped to the
// specs whose own test files moved (the INCREMENTAL gate); named specs are supported.
// --base <ref> sets the diff base.
// Exit 0: measured count-compatible, 1: drift, 2: incomplete/untrustworthy evidence.
import { readdirSync, readFileSync } from 'node:fs';
import { spawnSync, execFileSync } from 'node:child_process';
import { join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { measureEvidence } from './spec-baseline-evidence.mjs';
const root = fileURLToPath(new URL('..', import.meta.url));
export function parseArgs(argv) {
const options = { all: false, base: 'origin/master', named: [] };
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === '--all') { options.all = true; continue; }
if (arg === '--base') {
const value = argv[++i];
if (!value || value.startsWith('-')) throw new Error('--base requires a revision');
options.base = value; continue;
}
if (arg.startsWith('-')) throw new Error(`unknown option: ${arg}`);
options.named.push(arg.replace(/\\/g, '/').split('/').pop());
}
if (options.all && options.named.length) throw new Error('--all and named specs cannot be combined');
if (new Set(options.named).size !== options.named.length) throw new Error('duplicate named specs');
return options;
}
function childEnv() {
const env = {};
const keep = ['PATH', 'Path', 'SystemRoot', 'SYSTEMROOT', 'WINDIR', 'COMSPEC', 'PATHEXT',
'HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'TEMP', 'TMP', 'TMPDIR', 'LANG',
'LC_ALL', 'TZ', 'WIRE_BASE_TREE', 'WIRE_BASE_REQUIRED', 'WIRE_BASE_SHA', 'WIRE_BASE_TAG'];
for (const key of keep) if (process.env[key] != null) env[key] = process.env[key];
return { ...env, NO_COLOR: '1', WINDSURFAPI_SKIP_DOTENV: '1', RELOGIN_LIVE: '0',
GIT_TERMINAL_PROMPT: '0', GIT_CONFIG_NOSYSTEM: '1',
GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null' };
}
function loadSpecs() {
const names = readdirSync(join(root, 'test/mutations')).filter(n => n.endsWith('.json')).sort();
if (!names.length) throw new Error('zero discovered specs');
return names.map(name => {
const spec = JSON.parse(readFileSync(join(root, 'test/mutations', name), 'utf8'));
if (!Number.isSafeInteger(spec.expectBaselinePass) || spec.expectBaselinePass <= 0) throw new Error(`invalid expectation: ${name}`);
if (!Array.isArray(spec.tests) || !spec.tests.length || new Set(spec.tests).size !== spec.tests.length
|| spec.tests.some(f => typeof f !== 'string' || !/^test\/(?!.*(?:^|\/)\.\.(?:\/|$)).+\.test\.js$/.test(f))) throw new Error(`invalid test plan: ${name}`);
return { ...spec, name };
});
}
function selectSpecs(specs, options, env) {
if (options.named.length) {
const wanted = new Set(options.named);
for (const name of wanted) if (!specs.some(s => s.name === name)) throw new Error(`unknown spec: ${name}`);
return specs.filter(s => wanted.has(s.name));
}
if (options.all) return specs;
const git = args => execFileSync('git', args, { cwd: root, env, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
const base = git(['rev-parse', '--verify', `${options.base}^{commit}`]).trim();
if (!/^[0-9a-f]{40,64}$/.test(base)) throw new Error('invalid resolved base');
const changed = new Set([
...git(['diff', '--name-only', '-z', `${base}...HEAD`]).split('\0'),
...git(['diff', '--name-only', '-z', 'HEAD']).split('\0'),
...git(['ls-files', '--others', '--exclude-standard', '-z']).split('\0'),
].filter(Boolean));
// A synchronized checkout has no diff to scope by, and measuring everything is
// the only honest answer there. That is the tag/release shape.
if (!changed.size) return specs;
// Otherwise measure exactly the specs this diff can have moved: the spec file
// itself, or one of the test files it pins. A pin drifts when its own covered
// tests gain or lose assertions, so this is the selection that sees drift.
//
// This used to ALSO measure every spec whenever any src/, scripts/ or test
// helper changed — a guard against tests registered dynamically from source.
// It made the incremental gate pay the full-set cost (~100s) on nearly every
// branch and still did not make the fast path correct for the failure it was
// written for: a stale pin is a property of the spec's own test list. FULL
// coverage belongs to the release gate, which runs this script with --all (the
// release workflow passes it) — see the header comment.
return specs.filter(s => changed.has(`test/mutations/${s.name}`) || s.tests.some(f => changed.has(f)));
}
export function main(argv = process.argv.slice(2)) {
const options = parseArgs(argv), env = childEnv();
const specs = selectSpecs(loadSpecs(), options, env);
const policy = JSON.parse(readFileSync(join(root, 'scripts/spec-baseline-platform-skips.json'), 'utf8'));
if (!specs.length) { console.log('spec-baseline-check: no affected specs; no measurement claimed'); return 0; }
const reporter = pathToFileURL(join(root, 'scripts/spec-baseline-evidence.mjs')).href;
let drift = 0, untrustworthy = 0;
const receipts = new Map();
for (const spec of specs) {
const key = JSON.stringify(spec.tests);
let measured = receipts.get(key);
if (!measured) {
const result = spawnSync(process.execPath, [
'--import', './scripts/mutation-network-deny.mjs', '--import', './test/setup-env.mjs',
`--test-reporter=${reporter}`, '--test', '--test-concurrency=1', ...spec.tests,
], { cwd: root, env, encoding: 'utf8', timeout: 900000, maxBuffer: 64 * 1024 * 1024 });
try { measured = { value: measureEvidence(result, spec.tests, root, policy) }; }
catch (error) { measured = { error: error.message }; }
receipts.set(key, measured);
}
if (measured.error) {
untrustworthy++;
console.log(`UNVERIFIED ${spec.name}: ${measured.error}`);
continue;
}
const value = measured.value;
if (value.total !== spec.expectBaselinePass) {
drift++;
// The mutation harness refuses a run with this exact sentence
// (scripts/mutate-verify.mjs): a pin that no longer matches its suite means
// the suite moved under it, and both readers must name the two numbers the
// same way. The approved-skip breakdown is appended only when this platform
// skipped something, because the harness measures no skips at all.
console.log(`DRIFT ${spec.name}: baseline pass=${value.total} but spec expects ${spec.expectBaselinePass}. `
+ 'Either the suite changed or the spec is stale — both are worth knowing before '
+ 'trusting any verdict below.'
+ (value.skipped ? ` [${value.pass} pass + ${value.skipped} approved skips]` : ''));
} else {
console.log(`ok ${spec.name}: ${value.pass} pass + ${value.skipped} approved skips = ${value.total} [${value.disposition}]`);
}
}
console.log(`${specs.length} spec(s) measured, ${drift} drift, ${untrustworthy} untrustworthy`);
return untrustworthy ? 2 : drift ? 1 : 0;
}
if (resolve(process.argv[1] || '') === resolve(fileURLToPath(import.meta.url))) {
try { process.exitCode = main(); }
catch (error) { console.error(`spec-baseline-check: ${error.message}`); process.exitCode = 2; }
}