Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ src/
│ `rendering/pcb/outline-geometry.ts` and `backend/pcb/outline-geometry.ts`
│ are re-export shims over it; courtyard-rings.ts — the S12 per-side
│ courtyard regions (11 §2)
├── kicad-frame/ KiCad K→canonical frame helpers (`angles.ts` `norm360`,
│ `board-footprint-unplace.ts`) — see
│ docs/pcb-hardening/KF1-kicad-frame-contract.md
├── pcb-routing/ PCB routing
├── rendering/ re-export shim over @openpcb/rendering-core (see above) — EXCEPT the
│ in-tree `pcb/` (drills, outline kernels, chain-edges, loop-ring,
Expand All @@ -150,7 +153,7 @@ scripts/ module-cli.ts, gen-modules.ts, gen-sdk.ts, gen-contract-
tests/e2e/ Playwright
```

The shared tree has **nine** subtrees, listed above. There is no `src/shared/backend/`.
The shared tree has **ten** subtrees, listed above. There is no `src/shared/backend/`.

### Modules

Expand Down
239 changes: 239 additions & 0 deletions docs/pcb-hardening/KF1-kicad-frame-contract.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions docs/pcb-hardening/PROGRAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,16 @@ If quota becomes tight, the non-negotiable Astra sessions are **S1, S2, S5, S8,
S18** — where a false pass translates most directly into an electrically incorrect or
unmanufacturable board.

## Cross-item contracts

Contracts owned by cross-program items rather than a numbered session. They carry a non-numeric
prefix so session numbering is unaffected.

| Contract | Owner | Subject |
|---|---|---|
| `KF1-kicad-frame-contract.md` | H13 (OPENPCB-61) | KiCad K to canonical Y-up frame: conversion, placement composition, exactly-once rule, footprint frame provenance and qualification |
| H19 manufacturing-output contract (file name chosen by H19, non-numeric prefix) | H19 (OPENPCB-67) | Export routes, dialog and gates; consumes the KF1 frame-status blocker |

## Cross-cutting decisions and open items

- **`integ/trace-drag`** (6 commits, +1905/−7, trace-segment drag + 555-blinker fab fixture) must
Expand Down
338 changes: 338 additions & 0 deletions src/core/backend/tests/footprint-frame-predicate.test.ts

Large diffs are not rendered by default.

406 changes: 406 additions & 0 deletions src/core/backend/tests/kicad-board-footprint-unplace.test.ts

Large diffs are not rendered by default.

69 changes: 69 additions & 0 deletions src/core/backend/tests/kicad-frame-boundary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* H13 OP9 grep guard (H13 §3.1.4, KF1 §4): KiCad board/schematic Y is converted at
* ONE site. No designer KiCad-project import file may dereference a K-frame Y
* (`.yMm`, or a bbox `.minYMm` / `.maxYMm`) outside `kicad-frame.ts`,
* `frame-port.ts` or an allow-listed parser file.
*
* `PRE_H14_CALL_SITES` is a ratchet, not an exemption: the importer files H13 may
* not edit still convert verbatim until H14 S3/S6/S10 route them through
* `frame-port.ts`. Each entry must still exist and still dereference, so the list
* can only shrink.
*/
import { describe, expect, test } from "bun:test";
import { readdirSync, readFileSync, statSync } from "node:fs";
import path from "node:path";

const IMPORT_DIR = path.resolve(
import.meta.dir,
"../../../modules/designer/backend/import/kicad-project",
);
const FRAME_FILES = new Set(["kicad-frame.ts", "frame-port.ts"]);
/** Files that read KiCad source units on purpose (H14 census). None yet. */
const PARSER_FILES = new Set<string>([]);
const PRE_H14_CALL_SITES = new Map<string, string>([
["commit.ts", "H14 S6/S10 (outline bbox + polygon)"],
["insert-pcb.ts", "H14 S6 (placements, tracks, vias, zones, keepouts)"],
["insert-schematic.ts", "H14 S3 (symbols, pins, labels, wires)"],
]);
const K_FRAME_Y = /\.(?:y|minY|maxY)Mm\b/;

function sourceFiles(dir: string): string[] {
return readdirSync(dir).flatMap((name) => {
const full = path.join(dir, name);
if (statSync(full).isDirectory()) return sourceFiles(full);
return /\.tsx?$/.test(name) && !/\.test\.tsx?$/.test(name) ? [full] : [];
});
}

/** Comments may cite `.yMm`; code may not. */
function code(file: string): string {
return readFileSync(file, "utf8")
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/(^|[^:])\/\/.*$/gm, "$1");
}

describe("KiCad frame boundary (exactly-once rule)", () => {
const files = sourceFiles(IMPORT_DIR);
const rel = (f: string) => path.relative(IMPORT_DIR, f);

test("the frame helper exists and is the scanned tree's conversion site", () => {
expect(files.map(rel)).toContain("kicad-frame.ts");
expect(K_FRAME_Y.test(code(path.join(IMPORT_DIR, "kicad-frame.ts")))).toBe(true);
});

test("no other import file dereferences a KiCad Y", () => {
const offenders = files
.map(rel)
.filter((f) => !FRAME_FILES.has(f) && !PARSER_FILES.has(f) && !PRE_H14_CALL_SITES.has(f))
.filter((f) => K_FRAME_Y.test(code(path.join(IMPORT_DIR, f))));
expect(offenders).toEqual([]);
});

test("the pre-H14 allow-list only shrinks", () => {
for (const [file, owner] of PRE_H14_CALL_SITES) {
const full = path.join(IMPORT_DIR, file);
const stale = !files.includes(full) || !K_FRAME_Y.test(code(full));
expect({ file, owner, stale }).toEqual({ file, owner, stale: false });
}
});
});
Loading
Loading