B3 (editor): kicad-binding observes kdoc_meta.revertNonce (deduped, like seedNonce) → DOC_REVERTED_EVENT → WasmTool doc-reverted-toast (15 s, data-testid for the deferred e2e check). C: tests/tools/corpus-lint.ts (npm run corpus:lint) — kicad_tools --lint over the fixture corpora raw + shared-codec round-trips (docToFile(fileToDoc(x))); the E3/wrapInBoardEnvelope-class gate. Skips when output/kicad_tools.js isn't built; CI step lands when kicad_tools joins the CI app set. 7 fixtures / 7 round-trips green. S: --lint + --resave gain the .kicad_mod tier (pcbToolsLintFootprint / pcbToolsResaveFootprint; writer needs CTL_FOR_LIBRARY or the board flags strip the (version) header). Verified: qa footprint round-trip lints clean, resave stamps 20260206, garbage exits 1. Also bumps pcbjam-shared (mergeYUpdates for the bisect pass). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLua64PCVwkQ1hpWdaf1Gm
115 lines
4.1 KiB
TypeScript
115 lines
4.1 KiB
TypeScript
/**
|
|
* Corpus lint (kicad-validity 0001 C) — the E3 gate: our own s-expr PRODUCERS
|
|
* must agree with KiCad's parser. Runs `kicad_tools --lint` over:
|
|
*
|
|
* 1. the fixture corpus RAW (tests/fixtures + pcbjam-shared/test/fixtures)
|
|
* — guards fixtures themselves going stale vs the current parser;
|
|
* 2. every fixture ROUND-TRIPPED through the shared codec
|
|
* (docToFile(fileToDoc(text))) — the exact writer path the backend's
|
|
* ydoc materialization uses. A codec output KiCad rejects is the
|
|
* wrapInBoardEnvelope class of bug (pcbjam f07b997), caught pre-merge.
|
|
*
|
|
* Needs pcbjam/output/kicad_tools.js (docker/build.sh kicad_tools). When the
|
|
* artifact is absent the script SKIPS with exit 0 so it can sit in any CI leg;
|
|
* it becomes a hard gate once kicad_tools joins the CI build set (follow-up in
|
|
* docs/features/kicad-validity/0001 §5).
|
|
*
|
|
* Run: cd tests && npm run corpus:lint
|
|
*/
|
|
import { execFileSync } from "node:child_process";
|
|
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import * as path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { docToFile, fileToDoc } from "../../web/pcbjam-shared/src/index.js";
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const repo = path.resolve(here, "../..");
|
|
const cli = path.join(repo, "output/kicad_tools.js");
|
|
|
|
if (!existsSync(cli)) {
|
|
console.log("corpus-lint: SKIP — output/kicad_tools.js not built");
|
|
process.exit(0);
|
|
}
|
|
|
|
const CORPORA = [
|
|
path.join(repo, "tests/fixtures"),
|
|
path.join(repo, "web/pcbjam-shared/test/fixtures"),
|
|
];
|
|
const LINTABLE = /\.(kicad_pcb|kicad_sch|kicad_sym|kicad_mod)$/;
|
|
// The codec round-trips the doc formats it materializes (not symbol libs).
|
|
const CODEC = /\.(kicad_pcb|kicad_sch)$/;
|
|
|
|
function collect(dir: string): string[] {
|
|
if (!existsSync(dir)) return [];
|
|
const out: string[] = [];
|
|
for (const entry of readdirSync(dir, { withFileTypes: true, recursive: true })) {
|
|
const p = path.join(entry.parentPath ?? (entry as { path: string }).path, entry.name);
|
|
if (entry.isFile() && LINTABLE.test(entry.name)) out.push(p);
|
|
}
|
|
return out.sort();
|
|
}
|
|
|
|
function lint(file: string): { ok: boolean; detail: string } {
|
|
try {
|
|
execFileSync("node", [cli, "--lint", file], { stdio: ["ignore", "ignore", "pipe"] });
|
|
return { ok: true, detail: "" };
|
|
} catch (e) {
|
|
const err = e as { status?: number; stderr?: Buffer };
|
|
return { ok: false, detail: err.stderr?.toString().trim() ?? String(e) };
|
|
}
|
|
}
|
|
|
|
const files = CORPORA.flatMap(collect);
|
|
if (!files.length) {
|
|
console.error("corpus-lint: no fixtures found — corpus roots moved?");
|
|
process.exit(1);
|
|
}
|
|
|
|
const tmp = mkdtempSync(path.join(tmpdir(), "corpus-lint-"));
|
|
let failures = 0;
|
|
let roundTripped = 0;
|
|
|
|
try {
|
|
for (const file of files) {
|
|
const raw = lint(file);
|
|
if (!raw.ok) {
|
|
failures++;
|
|
console.error(`RAW FAIL ${path.relative(repo, file)}\n${raw.detail}`);
|
|
continue; // an invalid source can't meaningfully round-trip
|
|
}
|
|
|
|
if (!CODEC.test(file)) continue;
|
|
|
|
// Hierarchical sub-sheets don't parse standalone through the codec the
|
|
// same way; fileToDoc throwing marks "codec can't represent it" — that is
|
|
// a finding too, but a soft one: report, don't fail (multi-file sheets
|
|
// are exercised by the shared vitest suite with full projects).
|
|
let out: string;
|
|
try {
|
|
out = docToFile(fileToDoc(readFileSync(file, "utf8")));
|
|
} catch (e) {
|
|
console.warn(
|
|
`codec skip ${path.relative(repo, file)}: ${e instanceof Error ? e.message : e}`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const rt = path.join(tmp, `rt-${roundTripped++}${path.extname(file)}`);
|
|
writeFileSync(rt, out, "utf8");
|
|
const verdict = lint(rt);
|
|
if (!verdict.ok) {
|
|
failures++;
|
|
console.error(
|
|
`ROUND-TRIP FAIL ${path.relative(repo, file)} — codec output rejected by KiCad:\n${verdict.detail}`,
|
|
);
|
|
}
|
|
}
|
|
} finally {
|
|
rmSync(tmp, { recursive: true, force: true });
|
|
}
|
|
|
|
console.log(
|
|
`corpus-lint: ${files.length} fixture(s), ${roundTripped} round-trip(s), ${failures} failure(s)`,
|
|
);
|
|
process.exit(failures ? 1 : 0);
|