kicad-validity B3+C+S: revert toast, corpus lint, .kicad_mod tier
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
This commit is contained in:
parent
b18b0a6a08
commit
fb2aa2759d
8 changed files with 323 additions and 7 deletions
115
tests/tools/corpus-lint.ts
Normal file
115
tests/tools/corpus-lint.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* 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);
|
||||
Loading…
Reference in a new issue