9b395f5cc3
Build Chrome Extension / build (push) Waiting to run
Trigger Website Rebuild (Docs Updated) / dispatch (push) Waiting to run
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
119 lines
4.4 KiB
JavaScript
119 lines
4.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-typed-error-lint.mjs — CI gate for newly introduced silent failures.
|
|
*
|
|
* Baseline mode keeps adoption small: existing findings are recorded in
|
|
* scripts/typed-error-lint-baseline.json, while CI rejects any new
|
|
* silent-clamp / silent-empty-fallback / silent-sentinel signatures.
|
|
*/
|
|
|
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { dirname, resolve } from 'node:path';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const PROJECT_ROOT = resolve(__dirname, '..');
|
|
const DIST_AUDIT = resolve(PROJECT_ROOT, 'dist', 'src', 'convention-audit.js');
|
|
const BASELINE_PATH = resolve(__dirname, 'typed-error-lint-baseline.json');
|
|
const UPDATE = process.argv.includes('--update-baseline');
|
|
const RULES = new Set(['silent-clamp', 'silent-empty-fallback', 'silent-sentinel']);
|
|
|
|
if (!existsSync(DIST_AUDIT)) {
|
|
console.error('dist/src/convention-audit.js not found. Run npm run build before this check.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const { runConventionAudit } = await import(pathToFileURL(DIST_AUDIT).href);
|
|
const report = runConventionAudit({ projectRoot: PROJECT_ROOT });
|
|
const current = addOccurrenceIndexes(sortRecords(report.categories
|
|
.filter((category) => RULES.has(category.rule))
|
|
.flatMap((category) => category.violations.map(toBaselineRecord))));
|
|
|
|
if (UPDATE) {
|
|
writeFileSync(BASELINE_PATH, `${JSON.stringify(current, null, 2)}\n`);
|
|
console.log(`Updated ${relative(BASELINE_PATH)} with ${current.length} typed-error lint baseline entr${current.length === 1 ? 'y' : 'ies'}.`);
|
|
process.exit(0);
|
|
}
|
|
|
|
if (!existsSync(BASELINE_PATH)) {
|
|
console.error(`${relative(BASELINE_PATH)} not found. Run node scripts/check-typed-error-lint.mjs --update-baseline.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const baseline = addOccurrenceIndexes(sortRecords(JSON.parse(readFileSync(BASELINE_PATH, 'utf-8'))));
|
|
const baselineSignatures = new Set(baseline.map(signature));
|
|
const currentSignatures = new Set(current.map(signature));
|
|
const added = current.filter((record) => !baselineSignatures.has(signature(record)));
|
|
const resolved = baseline.filter((record) => !currentSignatures.has(signature(record)));
|
|
|
|
console.log(`Typed-error lint gate: current=${current.length}, baseline=${baseline.length}, new=${added.length}, resolved=${resolved.length}`);
|
|
|
|
if (resolved.length > 0) {
|
|
console.log('');
|
|
console.log('Resolved baseline entries detected. Consider shrinking the baseline:');
|
|
for (const record of resolved) {
|
|
console.log(` - ${record.rule} ${record.command} ${record.file}:${record.line}`);
|
|
}
|
|
}
|
|
|
|
if (added.length === 0) {
|
|
console.log('OK - no new typed-error lint violations.');
|
|
process.exit(0);
|
|
}
|
|
|
|
console.log('');
|
|
console.log('New typed-error lint violations:');
|
|
for (const record of added) {
|
|
console.log(` - ${record.rule} ${record.command} ${record.file}:${record.line}`);
|
|
if (record.text) console.log(` ${record.text}`);
|
|
}
|
|
console.log('');
|
|
console.log('Fix the silent fallback, or if this is an intentional baseline adoption, run:');
|
|
console.log(' node scripts/check-typed-error-lint.mjs --update-baseline');
|
|
process.exit(1);
|
|
|
|
function toBaselineRecord(violation) {
|
|
return {
|
|
rule: String(violation.rule ?? ''),
|
|
command: String(violation.command ?? ''),
|
|
file: String(violation.file ?? ''),
|
|
line: Number(violation.line ?? 0),
|
|
text: String(violation.details?.text ?? ''),
|
|
};
|
|
}
|
|
|
|
function signature(record) {
|
|
return `${record.rule}\0${record.command}\0${record.file}\0${record.text}\0${record.occurrence}`;
|
|
}
|
|
|
|
function sortRecords(records) {
|
|
return records
|
|
.map((record) => ({
|
|
rule: String(record.rule),
|
|
command: String(record.command),
|
|
file: String(record.file),
|
|
line: Number(record.line ?? 0),
|
|
text: String(record.text ?? ''),
|
|
occurrence: Number(record.occurrence ?? 0),
|
|
}))
|
|
.sort((a, b) => stableOrder(a).localeCompare(stableOrder(b)));
|
|
}
|
|
|
|
function addOccurrenceIndexes(records) {
|
|
const seen = new Map();
|
|
return records.map((record) => {
|
|
const key = `${record.rule}\0${record.command}\0${record.file}\0${record.text}`;
|
|
const occurrence = seen.get(key) ?? 0;
|
|
seen.set(key, occurrence + 1);
|
|
return { ...record, occurrence };
|
|
});
|
|
}
|
|
|
|
function stableOrder(record) {
|
|
return `${record.rule}\0${record.command}\0${record.file}\0${record.text}\0${record.line}`;
|
|
}
|
|
|
|
function relative(file) {
|
|
return file.replace(`${PROJECT_ROOT}/`, '');
|
|
}
|