Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
# The defect ledger
Every blocking defect we have hit on a real board, and **where the fix landed**.
The three example boards are the first three boards, not the deliverable. What
they are actually for is finding defect classes before a user does. So a defect
is not closed when the example stops failing — it is closed when a *future*
board cannot hit it.
Four places a fix can land, in descending order of value:
| Where | What it buys |
|---|---|
| **Planner** | the board can't be designed wrong in the first place |
| **Block / skeleton** | every board inheriting it is correct by construction |
| **`circuitlib`** | the number is right everywhere it is used, one owner |
| **Check only** | we find out afterwards, every time, forever |
**Check-only is not a fix.** It is a smoke alarm. A defect that is only detected
is still shipped by every user who ignores a warning, and still costs a build
cycle for every user who doesn't. It stays **open** in this ledger until it
moves up the table.
`scripts/shift-left-check` verifies that every fix claimed below actually exists
in the tree, and fails if one has been reverted or was never landed.
---
## Closed — a future board cannot hit these
| # | Defect | Found on | Fix landed in | Proof |
|---|---|---|---|---|
| 52 | **The planner's block-gap floor was one number for every block, and `rp2040-core` needed more than the others — three engineers discovered this independently, by hand, on the same day.** `BLOCK_GAP_MM = 2.0` is uniform in `circuitlib.layout`. `i2c-sensor-hub` hit a blocking DVDD-via clearance error (0.079mm against 0.09mm) *entirely inside `rp2040-core`'s own footprint* even at `10x`, fixed only by widening the board-level gap to its neighbour `i2c-bus` from 2mm to 5mm. `macropad-6` hit 46 blocking findings clustered on the SWD/RUN corner, cut to 14 by widening a different neighbour's gap 2mm to 6mm. Neither engineer's fix touched a net, a value or a via — both moved a competing block further away, and neither knew the other had done the same thing the same day. The planner that is supposed to make a board correct by construction was handing out the one number both of them had to override. | i2c-sensor-hub, macropad-6, desk-air-monitor (2026-08-17 EE reports) | **Planner** — `circuitlib.layout.BLOCK_GAP_OVERRIDE_MM = {"rp2040-core": 5.0}` (the smaller of the two confirmed-sufficient numbers), read by a new `pair_gap()` that every gap computation in the module now goes through: `place_row`'s cursor step, `place_board`'s row-wrap budget/row-to-row/edge-to-inner gaps, `overlap_warnings`'s collision check, and `min_board_for`'s floor estimate. A block that needs more room asks for it from either side of the pair, automatically, everywhere a gap is computed — not just the one call site an engineer happened to widen by hand. 5.0mm was chosen over macropad-6's 6.0mm because macropad-6's remainder (14, not 0) carried a second, different defect (an SWD trace crossing the RUN corner) that spacing alone does not close; i2c-sensor-hub's 5.0mm reached `fab.ready: true` on its own. Confirmed independently: i2c-sensor-hub's *committed* board (fixed by hand, before this change existed) already places `rp2040-core` and `i2c-bus` exactly 5.00mm apart, computed backward from its committed coordinates — the override matches what an engineer was forced to discover, to the millimetre. **Measured A/B, same composition (`Rp2040Core` + `I2cBus` + `DebugPort`), same `5x` effort, real KiCad-checked pipeline, nothing else changed:** 2mm gap → **13 blocking** (`pcb_trace_error` 1, `pcb_via_trace_clearance_error` 1, `pcb_placement_error` 1 — a via inside `U3.SWCLK`'s own pad — `dfm_hole_clearance` 2, `drc_violation` 8 including `[shorting_items] SWD and SWCLK` and a solder-mask bridge), `fab.ready: false`. 5mm gap → **1 blocking** (`pcb_placement_error`, a via inside `C5.pin1`'s own pad — a different part, a different net, the north decoupling row, not the SWD/RUN corner at all), `fab.ready: false`. 13 → 1, and the entire SWD/SWCLK/RUN-corner cluster that both EE reports describe is gone. **Re-measured after ledger #51 landed underneath this work** (a real 10x retry now fires where it did not before): rebuilding the 2mm-gap composition fresh under the fixed pipeline gave the identical 13 blocking, with `attempts: 2` in the sidecar and the pipeline's own note — *"routing was retried at autorouterEffortLevel=\"10x\" (up from 5x) and came back with 9 blocking finding(s) against 9, so the cheaper build stands... the remaining lever is the placement."* Effort did not move this defect class on this composition; spacing did. Never applied to any committed board — no board `.tsx` calls `place_board()`/`place_row()` at build time (every reference is a doc comment describing how the numbers already in the file were computed), so this cannot regress `desk-air-monitor`, `i2c-sensor-hub`, `rgb-lamp-controller` or any `examples/` board under the floor rule, and does not yet improve them either — it fixes the planner for the next board, not the six already built. Verified: `skills/circuitcode` test suite (128 tests) and `scripts/shift-left-check` both pass with the change. | `BLOCK_GAP_OVERRIDE_MM` / `pair_gap` in `circuitlib/layout.py` |
| 1 | Autorouter ran a GND track 0.115mm from the USB-C alignment drills; the fab needs 0.20mm. The router has no hole-clearance model at all. | all three | **Block** — a keepout sized to the receptacle's belly, inside the footprint so it travels with `pcbX`/`pcbY`/`pcbRotation` | `<keepout` in `usb-c-power.tsx` |
| 2 | A copper pour cuts a 32-sided polygon around every hole, so a nominal 0.2mm margin measures 0.1976mm at the chord midpoints — under the fab floor. Any board with any hole and a pour. | hydrate-coaster | **Glue block** — `POUR_CUTOUT_MARGIN_MM` derives the safe margin from the segment count, and the pour helper applies it | `POUR_CUTOUT_MARGIN_MM` in `blocks/glue.tsx` |
| 3 | `circuitlib.layout` stored each block's *size* and assumed its geometry was centred on the origin. It is not — `usb-c-data`'s copper sits 6.04mm above. Every board built on our own placement advice was wrong by millimetres before anyone wrote a line of TSX. | composition matrix (6/42 clean) | **`circuitlib`** — measured boxes relative to the origin, produced from real builds, plus `place_board()` which plans the whole board and returns its own warnings | `place_board` in `circuitlib/layout.py` |
| 4 | `autorouterEffortLevel` is a board prop with five settings and no CLI flag. We had never set it, so every board this project ever built routed at the default. At `"5x"`: terminal-keyboard 46 → 18 blocking, harness-puck 5 → 1, no design change. | all three | **Pipeline** — stage 0b retries at `5x` on a routing-class finding and keeps the retry only when it has strictly fewer blocking warnings | stage `0b` in `generation.py` |
| 5 | The planner could emit two USB-C entries on one board — `usb-c-data` is a superset of `usb-c-power` and reuses the same refdes block. | composition matrix | **Planner** — `board_plan` never picks both | the `usb-c-power` notes in `circuitlib/blocks.py` |
| 6 | `rp2040-core`'s crystal sat 11.78mm from XIN, past the 10mm limit, and the router silently skipped **the whole board**. | rp2040 boards | **Block** — placement corrected upstream | crystal placement in `rp2040-core.tsx` |
| 7 | A bare `<hole>` carries no keepout, so the router treats a mounting hole as free space. | all three | **Glue block + skeleton** — `MountingHole` ships the keepout, and a new project's first board already uses it | `MountingHole` in the project skeleton |
| 21 | The router's default effort produces boards that are not fab-ready, and **the escalation that exists to fix that cannot see the evidence**: stage 0b reads circuit.json, while the findings — `[clearance]`, `[shorting_items]`, `[hole_clearance]` — only exist after the KiCad cross-check three stages later. rp2040-core + `DebugPort`: `fab.ready` false with 5 blocking KiCad findings at default, true with 0 at `5x`, same design, same hour. | rp2040-core bench; all three boards | **Skeleton + skill + gauntlet** — every board declares `autorouterEffortLevel="5x"`, so the effort a board needs is a property of the board rather than a rescue the pipeline may not get to attempt. The escalation stays as the net for boards that do not. **`"10x"` was tried and dropped on measurement**: 46 minutes on harness-puck against 11 at `"5x"`, with the remaining findings unchanged in kind. A four-times serial cost for no measured gain is the wrong direction when the misses have a geometric fix (#28). | `autorouterEffortLevel` in the project skeleton |
| 22 | A via's *drill* was invisible to the hole-clearance gate — it only looked at component holes — so KiCad reported two violations at 0.132mm and 0.148mm on a board our own stage-4 gate called clean. Worse than a missed finding: a defect only KiCad can see arrives after the escalation gate, so the route is never retried. | rp2040-core bench | **`circuitlib`/profile + check** — `min_via_to_copper_mm = 0.20` (JLC "Via hole to Track"), and the gate now measures vias, SMD pads and tracks against every drill. Fires on the defect at stage 4a, in time to escalate; fires on none of the three boards. | `min_via_to_copper_mm` in `fab.py` |
| 23 | The hole-clearance gate modelled every drill as a circle of `hole_width/2`. The USB-C shell drills are 0.8 x 1.6mm **slots**, so copper off the end of one measured 0.4mm further away than it was. | reading the rule against the footprint | **Check** — drills, pads and slots are stadiums now, measured segment-to-segment | `_stadium` in `checks.py` |
| 24 | **The hole rule was applied to the hole's own pad**, so J1's shell-to-GND tie read as 0.006mm from J1's own GND drill and blocked two boards. That number is the annular ring. JLCPCB publishes "PTH annular ring >= 0.20mm" beside "PTH to Track 0.28mm"; both can be true only if the track figure measures copper arriving from outside. Measurement settles it: 28 segments across the three boards came within 0.28mm of a shell drill from outside its pad, and 25 of them measured 0.2000mm to four decimals — that is not geometry, it is where a connection crosses its own pad boundary, counted 25 times. A net-blind rule makes every plated hole unconnectable. KiCad's own hole-clearance DRC, on at 0.2mm and firing on NPTH holes on the same board, reported nothing at these pads. | harness-puck, hydrate-coaster | **Check** — same-net copper is exempt; with no net known either side, copper inside the pad's own outline is exempt; everything else still blocks. Both directions are pinned in the failure corpus. | `hole-rule-applied-to-the-hole-s-own-pad` in the failure corpus |
| 25 | The router halo (#21's sibling fix) was applied to **all four sides**, which put a USB-C receptacle's lowest copper 4.00mm inside the outline — a socket no cable reaches. The halo's evidence is about interior blocks; there is nothing to route in front of a connector. | `place_board` regression | **`circuitlib`** — the connector's face keeps `EDGE_MARGIN_MM`, the other three sides keep the halo | `face = EDGE_MARGIN_MM if edge` in `layout.py` |
| 9 | Silkscreen plotted at 0.033mm strokes; JLCPCB holds 0.15mm and thinner ink prints broken or not at all. **The real defect was worse than the symptom: all 54 silkscreen texts on the converted board are under JLCPCB's 1.0mm minimum height, 39 of them at 0.267mm, and none carries an explicit stroke — so KiCad derives one and plots it at a fifth of the floor. Every board this tool has ever made arrives with no readable reference designator.** | all three | **Pipeline (stage 3a)** — `kicad_normalize.normalize_for_fab` holds the converted board to the fab profile's silkscreen floors between the converter we do not own and the plotter. Idempotent, counted, reported. Measured after: 1864 of 1865 sub-floor strokes → 0 | `normalize_for_fab` in `circuitpy/kicad_normalize.py` |
| 8 | **The debug interface (SWCLK, SWD) reached no connector and no test point — the assembled board could not be halted, single-stepped or recovered from a bad image.** | all three | **Planner + glue block + skeleton.** The planner refuses the plan: `rp2040-core` declares `exposes=("SWCLK","SWD")`, `unexposed_nets()` reports what nothing brings out, `validate_board_law` makes it an **error**. `DebugPort` is what satisfies it — three 2.54mm pads, SWCLK/SWD/GND, beside `MountingHole` in the glue block. It is board furniture and **not** part of the MCU block, on measurement both ways: three pads inside `rp2040-core`'s own box send the debug pair through the crystal cluster and the router comes back with a via shorted into the QFN pad field; outboard, in open board space, the same design is `fab.ready` | `unexposed_nets` in `circuitlib/blocks.py`; `DebugPort` in `blocks/glue.tsx` |
| 13 | Copper sized for 604mA carrying 649mA, every segment one width. | harness-puck | **`circuitlib`** — `Block.peak_draw_ma` / `peak_per_unit_ma` give the planner a worst case (a WS2812 idles at 4mA and pulls 60mA), `board_plan` turns it into `power_trace_width_mm` by IPC-2221, and declaring less is an error | `power_trace_width` in `circuitlib/helpers.py` |
| 14 | An LDO reaching ~96 degC junction at the downstream chain's datasheet peak. | harness-puck | **Planner** — the same peak drives `regulator_thermal` at a 45 degC ambient, and a plan that would cook the part has `buildable = False`. Twenty WS2812s behind an AMS1117 is 1.3A and 183 degC: that board can no longer be planned | `overheats` in `circuitlib/helpers.py` |
| 15 | The pour-margin fix from #2 was structural but the number was still written down in three places: `hydrate-coaster` hardcoded `cutoutMargin="0.25mm"` on two pours, and two `BLOCK.md` files gave it as advice to remember. A number written down three times will disagree in two of them. | hydrate-coaster | **Delete the copies** — the pours pass `POUR_CUTOUT_MARGIN_MM`, and both BLOCK.md files now point at the constant instead of quoting it. `scripts/shift-left-check` fails if a literal comes back. | no `cutoutMargin="0.25mm"` literal outside the constant |
| 26 | **The supplier-footprint IoU band called eight identical capacitors a footprint mismatch because they were rotated.** Same part C1525, same `footprint="0402"`: thirteen at rotation 0 scored 0.7249, eight around an LED ring at multiples of 22.5 degrees scored 0.4739-0.4916, under the 0.5 error floor. The kind blocks, so **any board with an off-axis passive could never be fab-ready** — every circular layout, angled connector and diagonal-edge part, permanently un-orderable, and it looks like a real DFM problem every time. A bench of one part at six angles settles what the boards suggested: 0.7249 at 0, 90, 180 and 270; 0.4739 at 22.5; 0.4215 at 45. The metric is compared axis-by-axis, so it survives a quarter turn and collapses off-axis. | harness-puck | **Check** — orthogonal parts are graded in full; an off-axis part keeps its measurement at `info` with the angle and the bench numbers attached, reported rather than dropped. Lowering the floor past 0.47 was refused: it would keep the metric wrong and widen the hole for genuinely mismatched parts. Both directions are in the failure corpus, which now runs the IoU bander at all — it did not, which is how eight capacitors got there unopposed. | `footprint-iou-graded-through-a-rotation` in the failure corpus |
| 28 | **Two levers were pulled the wrong way for three hours because only the finding count was read, never the setting beside it.** (a) *Router effort is not monotonic.* harness-puck: 2 blocking at `10x`, 2 at `5x` — and 46 minutes against 11. hydrate-coaster: 2 at `10x`, **1** at `5x`. More search produced an equal or worse board at four times the wall clock. (b) *A smaller via pad made it much worse.* Reasoning from geometry, 0.50mm instead of 0.60mm buys 0.05mm of clearance per side and the remaining misses were all within 0.02mm of the floor; measured, harness-puck went **2 blocking to 17** — tracks crossing, five accidental contacts. The freedom was spent, not saved. | harness-puck, hydrate-coaster | **Both reverted, and the method changed with them.** Every effort or geometry knob is now quoted with its blocking count — `(effort, count)` — because a lever read one-sided is a lever nobody can tell is backwards. `5x` and a 0.60mm via pad are what the boards ship with, on measurement rather than on the argument that predicted otherwise. | `autorouterEffortLevel="5x"` in the project skeleton |
| 29 | **The schematic export drew every key as a short.** The 4-pad pushbutton renders on a 2-pin symbol, and `sw-tact`'s redundant same-net tie drew as a wire looping across it — the first human EE review read all 50 terminal-keyboard keys as dead. The copper was right; the artifact lied, and every representation (review SVG/PNG, KiCad export) inherits the same source, so no downstream check could see it. | terminal-keyboard, EE review 2026-08-15 | **Block + check** — the pushbutton declares `internallyConnectedPins` {1,2}/{3,4} (LCSC's own symbol for C318884 draws the bars; EasyEDA API, 2026-08-15), so every export folds same-terminal pins onto the symbol's two terminals and the loop is never drawn. Wiring went **diagonal** (signal → pin 1, pin 4 → to): pins 1/4 are opposite terminals under either side-pairing, so the "every board using it is scrap" pairing bet is gone from copper entirely — first-article continuity stays as the final check. `schematic_truth_warnings` (stage 1) flags a wire drawn between two pins of any switch symbol, on every board, forever. rp2040-core's BOOTSEL/RESET carried the same schematic defect and got the same `internallyConnectedPins` fix; its *copper* stays 4-tie **on measurement** (#28 method) — the diagonal rewire there landed a via 0.0787mm from the crystal trace, 5/5 runs, and same-group ties are safe under the confirmed pairing. | `internallyConnectedPins` in `sw-tact.tsx`; `schematic_truth_warnings` in `checks.py` |
| 30 | **The planner could not see fab price tiers.** 112×90mm quotes $8.90 for 5 sample boards; under 100×100 the same order is $2. Nothing at plan time knew the cliff existed, so nobody tried to fit it — 12mm cost 4.5×. | terminal-keyboard, EE review 2026-08-15 | **Planner + fab profile + check** — `JLC_SAMPLE_TIER_MM` in `circuitlib/tables.py` and `price_tier_warnings` inside `place_board`, so every plan is told the money before a board exists; `price_tiers` on the fab profile and `dfm_price_tier` at stage 4 name the overage and the measured quote on any board that misses every tier; ORDER.md's cost lines carry the cliff. **terminal-keyboard itself is now 100×90** (2026-08-16, Dee approved the body change): the "content spans 107.7mm" that read as a wall *was* the mounting holes — switch pads span 97.00mm and all copper 99.00mm, while the M2.5 drills span 107.70mm and their keepouts 108.20mm. Three moves, no pitch change: diodes tuck 2.0mm right (at −3.4 they hung 0.75mm off a 100mm board), the status LED shifts 2.2mm east (its courtyard was 0.320mm from the new edge), and all six screws leave the side rails for the top and bottom rails at x = −47/0/+47, y = ±42. No screw fits beside the key field at any pitch above 9.62mm, so the side bosses are gone from the printed body and the load moves to a rib on the bare underside — all 137 parts are top-side. Measured on the same pipeline, same command: **12 blocking → 1**, `dfm_price_tier` gone, 5 bare boards $8.90 → $2.00. | `price_tier_warnings` in `circuitlib/layout.py`; `price_tiers` in `fab.py` |
| 32 | **BOM/CPL ignored the consumer's spec.** JLC documents an exact sample shape — designators of one part grouped on one line, Rotation before Layer in the centroid — and we emitted what our tools produced naturally: one row per placement (50 identical switches = 50 lines) and the exporter's column order. | terminal-keyboard, EE review 2026-08-15 | **Exporters** — `group_bom_rows` gives every BOM one line per part with natural-sorted grouped designators (a part with no identity keeps its own line — two unknowns are not the same part); the CPL writer follows JLC's documented column order; `bom_summary` counts the grouped lines ORDER.md quotes while cost stays per placement. Golden-file tests pin the exact bytes. Applies to every board the app ever exports. | `group_bom_rows` in `fab.py`; `test_bom_golden_file_jlc_sample_shape` |
| 33 | **A polygon pad whose ring is closed reports every via on the board as inside it.** A polygon pad written the way every geometry format writes one — last vertex repeating the first — carries exactly one zero-length edge, and `@tscircuit/checks` resolves "is this via inside this pad" with a ray cast whose boundary shortcut `isPointOnSegment` answers **true for every point in the plane** against a zero-length segment: dot product 0, squared length 0, so `0 <= 0 + 1e-9` passes. The pad contains the board, and whichever via the spatial index reaches first is reported inside it. This is what blocked hydrate-coaster — `pcb_placement_error` on J1.B4A9 — and the geometry disproves the finding outright: the named via's center sits **0.3009mm above that pad's top edge**, with 0.0009mm of clear copper between its own annulus and the pad, and all 108 vias measured against all 174 pads gave **zero** containments. It is a landmine on every board with a USB-C port, not one board's bug: harness-puck and terminal-keyboard carry the same four closed rings and passed only because no via of theirs landed in the same spatial bucket as a connector pad. Two of three boards were fab-ready by luck. | hydrate-coaster; latent on all three | **Block + pipeline (stage 0a).** The golden block opens its four rings at source, so the library cannot ship one — the copper is identical, an open ring and a closed one describe the same polygon. And `normalize_circuit_json` opens any closed ring in the compiled circuit.json before anything reads it, because the shape arrives from outside too: a footprint imported from EasyEDA or KiCad closes its rings, and a user who pastes one in would hit the same false block with nothing to tell them why. The repair drops only the `pcb_placement_error` its own edit invalidates, judged on the most generous test available — the via center is not inside the pad's **bounding box** — so a genuine via-in-pad can never be dropped, and the corpus pins that direction too. A board with no closed ring is not rewritten at all, so the canonicaliser's byte conservatism survives wherever the defect is absent. Measured: `runAllChecks` on hydrate's circuit.json goes 2 findings to 1 with the rings opened, and the survivor is a trace length. The two frozen example snapshots (harness-puck, terminal-keyboard) still carry closed rings in their own `blocks/` copies; stage 0a covers them until they re-vendor. | `def normalize_circuit_json` in `circuitpy/circuit_normalize.py` |
| 34 | **Fix 1 reached the artifact; three other lies on the same artifact did not.** Verified on the 2026-08-15 export (build 22:07-22:26, later than every source file): SW10 renders as a two-pin switch, **pin 1 on K00, pin 2 on ROW0**, no wire across the contacts — all 50 keys, confirmed in KiCad's own netlist and by looking at the rendered symbol. The fold happens in tscircuit's schematic layout, not in our converter, which is why the converter has no reference to `internally_connected_source_port_ids` and the fix worked anyway. But the same file also carried: **`J1`, the USB-C receptacle, drawn as nothing at all** (`schematic_component.symbol_name` is null, the converter emits a `lib_id` it never defines, KiCad drops all 16 pins on the origin and reads VBUS/GND/D+/D− as **one net**); **`SW2` and `SW3` — BOOTSEL and RESET — with pin 1 dangling**, so the two buttons that kept 4-tie copper read as dead; and 13 more pins the design connects that KiCad reads as unconnected. An EE opening tomorrow's packet meets a shorted connector and two dead buttons. | terminal-keyboard, harness-puck, hydrate-coaster | **Pipeline (stage 3b)** — `kicad_schematic.normalize_schematic_truth` is `kicad_normalize`'s sibling for the schematic: it synthesises a body for any `lib_id` the converter references and never defines, pulls apart symbols stacked on one point, then compares KiCad's own netlist against `circuit.json` pin by pin and stamps the true net name where they disagree. Three sub-defects were found by measuring rather than reasoning. (a) *The drawn pin's identity is its **name**, not its number*: `Device:crystal_4pin_right` runs num 1..4 against name 4,2,1,3, so the number rule put XIN's label on a ground leg — the name resolves 239/239 pins on the three boards and disagrees with the number on exactly those 3. (b) *KiCad multiplies mm by 10000 to reach internal units and we divided by 0.0001*, which is a different computation in binary: 424.83124999999995mm is 4248312.5 IU multiplied (rounds up) and 4248312.499999999 divided (rounds down). One unit of 100nm was enough that **16 correct labels on harness-puck's J1 bonded to nothing** while the identical code repaired terminal-keyboard, whose coordinate happens to snap the other way. (c) *Three symbols on one point cannot be repaired with labels at all* — hydrate-coaster's TP1/TP2/TP3 all sat at (660.125, 496.5541), tying SWCLK, SWD and GND together, and a labelling pass loops forever on it; they get moved apart. Measured independently (KiCad's netlist against a union-find over circuit.json, not the pass's own report), before → after: nets covering >1 design net 0/0/1 → 0/0/0; design nets drawn as >1 2/4/2 → 0/0/0; pins the design connects that KiCad calls unconnected 14/15/15 → **0/0/0**. 6-12s a board, byte-identical on a second run. | `normalize_schematic_truth` in `circuitpy/kicad_schematic.py` |
| 35 | **No two builds of the same board ever produced the same file — and the route was never the reason.** "Fab-ready" was a property of a lucky run rather than of the design, which blocked caching, golden files and any regression test on a packet. The suspected cause was the router's wall clock: fewer solver steps fit into a slice on a busy machine, so the search lands somewhere else. Measurement says otherwise. hydrate-coaster at `"1x"`, six builds — three quiet at 95.9/95.5/97.6s and three with 16 busy processes alongside at 291.9/273.9/288.7s, a **2.9x wall-clock spread** — returned the **byte-identical route all six times** (`pcb_trace` + `pcb_via`, ids included), and **six different files**. Reading the code says the same thing: `tscircuit-cli` runs one 25.9MB bundle that requires nothing from node_modules, so the standalone `@tscircuit/capacity-autorouter` package where the clock calls were counted is never loaded; inside the bundle every solve loop is `for(;!this.solved&&!this.failed;)this.step()` against MAX_ITERATIONS, and the single real time budget in the whole bundle (PackSolver2's) reads `platform.pcbPackSolverTimeoutMs`, which nothing sets. The actual cause is the parts engine: 27 of hydrate-coaster's 1812 elements are `supplier_footprint_mismatch_warning`s, the only elements in the file whose id is not a counter, appended in **network-completion order** with a random 10-character id each. | all three | **Vendored toolchain patch + pipeline.** `toolchain/determinism/deterministic-run.mjs` replaces `Math.random` with a seeded generator, seeded from the board's own source fingerprint — a `NODE_OPTIONS --import` preload rather than a patched package, because the package is not what runs and forking a 25.9MB bundle would have to be redone on every version bump. Seeding fixes the id *stream*; it cannot fix *who gets which id*, so `_canonicalise_file` (stage 0, before anything reads the file) puts the racing elements in content order inside the slots they already occupy, and refuses any group whose ids are referenced, whose members are counter-numbered, or whose rewrite would not be a pure permutation. The preload **announces itself on stderr and the pipeline reports what it saw**, not what it asked for: `NODE_OPTIONS` is ignored the moment `tscircuit-cli` picks bun as its runner, and a flag that reports intent is how a fix goes quietly missing. Wall clock stays only as a safety valve — the 600s build budget and the 1500s escalation budget were both smaller than a loaded machine needs, so the machine, not the design, decided which route shipped; both are 5400s now. After: **one file hash across three quiet and three loaded builds**, and the route hash is unchanged from before the fix, so seeding moves no copper. **The boundary is the parts lock, and it is measured**: hydrate-coaster pins its 19 parts in `parts.json`, takes zero supplier round trips and is byte-identical; terminal-keyboard pins nothing, so every part goes to the catalogue and two builds came back with **47 and 17 `HTTP 429` rate-limit failures** — a different subset of parts resolved each time, 5459 elements against 5441. Its *route* is identical across all of it (`14d0f071`, five builds, 298–494s), but the two files are not builds of the same board and no seed can make them equal. Pinning terminal-keyboard's and harness-puck's parts is the rest of this fix, and it belongs to `parts-book`. | `_canonicalise_file` in `generation.py`; `toolchain/determinism/deterministic-run.mjs`; `toolchain/determinism/run_repeat.py` reproduces the six-build measurement and names the network when it is the cause |
| 38 | **The autorouter solves D+ and D− as two unrelated nets that happen to share a name.** EE review 2026-08-15, finding 3. Measured at 52d0803: hydrate-coaster 51.71/37.80mm, **7%** of the run coupled, widest gap 15.21mm; harness-puck 68.71/49.49mm, 1%, 16.98mm; terminal-keyboard 50.91/40.28mm, 25%, 7.76mm — against a straight-line distance of 23.05mm on hydrate. Two further blind spots came out with it: the coupling check matched its P/N token only at the *end* of a net name, so **`USB_DP_CONN`/`USB_DM_CONN` — the half of the pair closest to the cable — was never measured on any board**; and nothing measured the third thing the EE asked for, a return plane, which is **0% on all three boards**. | all three, EE review 2026-08-15 | **Pipeline (stage 0c) + check.** `circuitpy.diffpair.route_diff_pairs` runs after the autorouter and before anything reads the file: it deletes the pair's copper, finds one **centreline** with A* through a mask dilated by the pair's own half-span, and lays the two legs as that line offset both ways — parallel and constant-gap because they are the same polyline. Layer changes are allowed (a two-layer board with parts on top often has no top-side corridor at all: hydrate's is a 7.6mm² pocket) with one via per leg, and a **crossover** where the pair must swap sides — staggered vias, congruent diagonals, no skew added. It **refuses rather than regress**: every new segment is measured against every pad, drill, via, keepout, pour, other-net trace and the board edge at the fab profile's own floors, and the pair is only kept when coupling improves, skew does not, and clearance holds. Measured on hydrate-coaster: coupling **7% → 95%**, skew **13.91mm → 1.95mm** (inside the 3.8mm USB budget), lengths 51.7/37.8 → 37.0/39.0mm, worst clearance **0.115mm → 0.128mm**, 8 vias out and 8 in, 8s. harness-puck and terminal-keyboard refuse and say why. **Why it is not seeded best-of-N**: the capacity autorouter has no randomness — all 86 `Math.random` sites in the 25.9MB CLI bundle are id generation, React, three.js, undici and brace-expansion, none within 200kB of the autorouter, which agrees with #35's six-build measurement. **Why it is not pre-routing**: `pcbPath` does pin a trace's copper and take it out of the autorouter's list, but `getSimpleRouteJsonFromCircuitJson` builds obstacles from components, pads, plated holes, holes, vias and cutouts — **`pcb_trace` is not in that list**, so copper laid before the autorouter is copper it routes straight through. **And the pass is graded by the pipeline's gate, not by its own model** — which is what caught the first version. That one measured every clearance rule it had written down, passed its own tests, and produced a board with **eight `dfm_hole_clearance` errors and five KiCad hole-clearance violations**: at a 0.57mm pair pitch, one via's *pad* sits 0.196mm from the other via's *drill* against a 0.20mm rule. Pad-to-pad was legal and drill-to-drill was legal; the cross term was the one nobody had modelled. Two things landed from that: the pitch is now the max of all three rules (0.595mm, still inside the 4-width coupling budget), and `route_diff_pairs` takes a `grade` callback — the pipeline hands it `harvest_circuit_json` + `run_tscircuit_checks` + `dfm_warnings` and the rewrite is kept only when it adds no blocking finding the board did not already have. A gate that cannot run is a refusal, not a pass. **End to end on hydrate-coaster, same source, one build each**: `fab.ready` **true → true**, 0 errors → 0 errors, KiCad `drc_violation` 150 → 150, total warnings **166 → 165** — the pair's skew and coupling warnings both gone, and one new warn-band `dfm_annular_ring` in their place. That last one is a real tradeoff and the arithmetic is closed: the pair's own vias sit at the pair's pitch, so the pitch has to clear pad-to-pad, pad-to-drill and drill-to-drill *and* stay inside the coupling check's 4-width budget (0.60mm on 0.15mm copper). A via that trips none of the profile's warn bands needs a 0.50mm pad on a 0.30mm drill, which forces a 0.62mm pitch — outside the budget, so the pair could not change layers at all and hydrate-coaster would have no route. 0.45mm on 0.30mm keeps the preferred pad and the preferred drill and misses only the preferred annular ring, at 0.075mm — the fab's floor, not below it. | `route_diff_pairs` in `circuitpy/diffpair.py`; `netclass_pair_reference` in `verifylib/netclass.py` |
| 41 | **The hole-clearance gate did not know what shape a pad is, and the last blocking finding on the last board was a pad that is not there.** It modelled every SMD pad as the stadium inscribed in `width` x `height`, on the x-axis, and dropped the pad's own `ccw_rotation`. That does not blur a shape, it **moves** one. terminal-keyboard's U4 is a W25Q128 in SOIC-8: eight 2.25 x 0.63mm pads at `ccw_rotation: 90`, so the copper is 2.25mm *tall*. Swung back onto the x-axis it becomes 2.25mm *wide*, reaches 0.81mm sideways toward `pcb_via_200`, and reports **0.130mm against a 0.20mm floor**. The real gap is **0.506mm** — 3.9x the reported number and 2.5x the rule. Three independent witnesses: the 1.27mm pad pitch (eight pads 2.25mm wide at 1.27mm centres would overlap each other, so the unrotated reading cannot describe any real footprint); the KiCad packet, which writes every U4 pad with a trailing `90` in its `(at ...)` and `(size 2.25 0.63)`, so the plotted copper is the rotated one; and KiCad's own hole-clearance DRC, on at 0.2mm on the same packet, which reports **nothing** anywhere on the board. Reading the geometry took one minute and would have on day one — lesson C again, one shape further out. **Two more blind spots came out with it, both silent**: a `circle` pad carries `radius` and no `width`, and `width or radius` fed a *radius* into a *width*, so every round pad on every board was modelled at **half size** (the DebugPort test points are 1.0mm across and were measured as 0.5mm); and a `polygon` pad carries neither, failed the numeric guard, and **was never measured at all** — including the four shell pads on every USB-C receptacle we ship, which sit beside the exact drills this gate exists for. One invented finding, two hidden ones. | terminal-keyboard, 2026-08-16 | **Check** — `_pad_copper` reads the pad the fab plots: a rectangle keeps its corners, a pill is a stadium, a polygon is its own outline, a circle is its own radius, and every one of them is turned by its `ccw_rotation`. Drills get the same treatment. The exact model is *stricter* than the inscribed stadium it replaces (a 1.0mm square pad's corner protrudes 0.207mm past it), so it was measured before it was believed: **all three boards, 0 findings under the exact model, 0 under the old one plus the phantom** — the ruler got sharper in both directions and no board paid for it. Both directions are pinned in the failure corpus (`a-pad-measured-unrotated`: the same 0.130mm gap fires when the pad's long axis genuinely points at the drill, and terminal-keyboard's own geometry stays clean), and the three shape readings have unit tests. **`dfm_edge_clearance` had the identical blind spot and got the identical reading** — it too took `width`/`height` unrotated and skipped every polygon pad, which on these boards means the four USB-C shell pads, at the edge by design; measured the same way, the only change on the three boards is that those twelve pads are examined at all and all twelve pass. Free speedup as a side effect: a bounding-disc reject before the exact measurement takes the hole gate from **2.5s to 0.16s** on terminal-keyboard. | `_pad_copper` in `circuitpy/checks.py` |
| 43 | **Every BOM this project has ever shipped went out with a blank Footprint column, and the catalog that fills it was never missing.** `_catalog_packages` found the JLCPCB mirror with one hard-coded hop — `Path(__file__).resolve().parents[4] / "packages" / "parts-catalog" / "catalog"`. In the repo, `packages/circuitpy/src/circuitpy/` is four levels under the root and that is correct. In `skills/circuitcode/scripts/packages/circuitpy/` — **the runtime a build actually imports** (#42) — four levels up is `skills/`, so it looked for `skills/packages/parts-catalog/catalog`, which has never existed. The function's own docstring said "missing catalog is not an error — the columns simply stay blank, which is the honest answer", and that sentence is what hid it: blank is only honest when it means *unknown*, and here it meant *we did not look in the right place*. The catalog has the packages — C81598 → SOD-123, C318884 → SMD-4P 5.1x5.1mm, every 0402 — and JLC's parts-match table uses that column as the one cross-check between the number you typed and the part you meant. The EE's finding 6 asked for JLC's format; we shipped the format with the content missing. | all three, 2026-08-16 | **Resolver + check.** `fab.catalog_root()` *searches* for the directory instead of computing it — every parent of `__file__`, under both the repo layout and the vendored one, plus a `CIRCUIT_PARTS_CATALOG` override — and returns None when there genuinely is none. And the silence is now impossible: when the root is missing the build emits `bom_catalog_missing` naming the consequence and the fix, because a blank column that nobody is told about is a wrong answer wearing an empty cell. A test asserts the catalog resolves *and is non-empty*, which fails from either tree. Two smaller gaps on the same artifact went with it: TP1/TP2/TP3 shipped as three BOM lines with no part number — bare copper handed to the fab as three unbuyable parts — and are now excluded by `_is_placeable` (narrowly: only definitionally-bare ftypes, because "has no part number" would hide a real part nobody has sourced yet, which is the shortfall the packet exists to report); and harness-puck's CPL carried a rotation of `202.49999999999994`, now wrapped into [0, 360) and printed to 4 decimals. | `catalog_root` in `circuitpy/fab.py`; `test_the_catalog_is_found_from_the_vendored_runtime_too` |
| 44 | **Two of the buttons the EE called shorted really were drawn shorted, and we had recorded it as a false alarm.** Fix 1 (#29) declared `internallyConnectedPins` so a same-net tie folds into the switch symbol instead of drawing as a wire, and it worked — on the 50 keys, which come from `sw-tact`. `rp2040-core` wires its own BOOTSEL and RESET buttons inline, and one of those lines is literally `<trace from=".SW2 > .pin2" to=".SW2 > .pin1" />` — a trace whose two endpoints are pins of the same component. `schematic_symbol_short` fired on SW2 and SW3 on **all three boards** at every rebuild, and the review doc wrote it off as the smoke alarm reading circuit.json rather than the export. Half right, and the wrong half is the one that matters: the KiCad export does fold it (SW2's two drawn pins carry two different net labels, verified on the shipped `kicad-project.zip`), but the tscircuit schematic SVG **is** an artifact we ship, it renders `schematic_trace` directly, and that is where the loop across the contacts is drawn. The reviewer's own screenshot is that render. A check firing on every board for two days was called noise because one of the two artifacts it could have been about was clean. | all three, 2026-08-16 | **Block** — the ties go through a named net (`net.BOOTSEL_SW`, `net.RUN_SW`) instead of pin to pin. Identical connectivity, identical copper, identical net classes; the difference is that two traces to a net are drawn as two labels while a trace between two pins of one symbol is drawn as a loop across it. The 4-tie copper stays, on the measurement that put the diagonal rewire's via 0.0787mm from the crystal trace. **The general rule: a same-net tie between two pins of one component must be expressed as a net, never as a pin-to-pin trace** — the renderer cannot tell "these are one terminal" from "somebody shorted this". | `TR_SW2_p1`/`TR_SW3_p1` in `rp2040-core.tsx`; `schematic_symbol_short` |
| 10 | Mask openings separated by less than the 0.2mm sliver minimum; narrowest 0.114mm. | all three | **Not a defect — the check was wrong.** All ten sub-0.2mm webs sit inside *one part's own land pattern*: 0.114 and 0.157mm inside the USB-C receptacle's footprint, 0.1985mm inside each of eight 0402s, which is simply what a 0402 land pattern is. Those dams are specified by the package and JLCPCB builds them daily. The check is now scoped to webs between *different* parts — where nobody qualified the geometry — and that version fires on none of the three boards. Escalating the original would have made every board permanently un-orderable over a standard passive | `_mask_slivers` scoping in `verifylib/gerber_truth.py` |
---
## Open — detected, but a user can still hit them
These are the ones that matter now. Each is currently check-only.
| # | Defect | Found on | Where the fix belongs |
|---|---|---|---|
| 53 | **Even at the wider board-level gap (#52), `rp2040-core`'s own interior is still tight enough to produce a via genuinely inside a pad — just a different pad.** After the gap fix, the one surviving blocking finding on the A/B bench was a via inside `C5.pin1` (a 100nF decoupling cap on the *north* decoupling row, `pcbY=6`, nowhere near the SWD/RUN corner both EE reports describe) — the same defect *class* as macropad-6's `U3.RUN`/`U3.SWCLK` via-in-pad findings, on a different pad the router happened to squeeze into this time. Traced macropad-6's actual finding to its source: the via sits on `TR_U_run`, the block's *own* single-net escape trace from `U3.RUN` to `R12` — and `R12` (the RUN pull-up) sits at local `(-8, -6)`, **9.8mm** from `RUN`'s real pad location (`local (1.8, -3.4)`, on the QFN's south edge next to `SWD`/`SWCLK`). The mirror-symmetric `R13` (BOOTSEL series) is worse: it sits at `(8, -6)` while the pin it serves, `QSPI_SS`, is actually at `local (-2.6, +3.4)` — the *opposite* quadrant. Both resistors were placed for left/right visual symmetry with their buttons, not for proximity to the pins they wire to, and the escape route has to cross the block's own densely packed decoupling row (5 caps, 3mm pitch, x=-6..+6, y=-6) to get there — which is what forces the via. Separately, the block's own golden bench (`test_bench_builds_clean[rp2040-core]`) has been red since 2026-08-11 on a related but distinct finding: `Y1.pin1`→`U3.XIN` **routes** at 12.46mm against the 10mm crystal-connection maximum (straight-line is only 7.94mm — the difference is a detour around the same decoupling row, through two extra vias). Tried and did not converge, applied nowhere in the tree: moving `R12` nearer `RUN` collided with the SWD/SWCLK escape corridor or the QFN's own courtyard depending on exactly where; shifting the whole crystal cluster to open a channel through the decoupling row either hit `Y1`'s own courtyard against `C11` or pushed a *different* pin (`C15`→`XIN`) straight past the 10mm hard cap, autorouting-skipped-for-the-whole-board (ledger #6's failure mode). The router's response to small nearby placement changes was not monotonic — a 1.8mm shift made the XIN warning worse, not better — consistent with ledger #28. Nothing here was committed; all of it lives in a throwaway scratch copy, never `packages/golden-blocks`. | macropad-6 (`U3.RUN`/`U3.SWCLK`), the ledger #52 A/B bench (`C5.pin1`), rp2040-core's own gauntlet bench (`Y1.pin1`→`XIN`, since 2026-08-11) | **Block.** `R11`/`R12`/`R13` and the decoupling row need placing from each resistor's *actual* pin location rather than from left/right visual symmetry — the QFN's real pin geography (measured from a real build, not the schematic) has to be the input, the way ledger #3 made block *boxes* measured rather than assumed. Whoever does this needs a real routed build to validate against (the isolated golden-blocks testbench does **not** reproduce macropad-6's `U3.RUN` finding — it only appears once a board-level trace like the SWD-to-debug-port run competes for the same corridor — so the golden-blocks gauntlet, ~107s, is a necessary but not sufficient proxy; a full product-board rebuild is still the only reliable check). |
| 55 | **The gap fix (#52) closes the defect class outright on a small board and only shrinks it on a dense one — confirming #53's diagnosis on two more boards, measured, not argued.** `BLOCK_GAP_OVERRIDE_MM` fixed the planner; no board calls `place_board()` at build time, so it did nothing for a shipped board until someone re-derived its literal `pcbX`/`pcbY` and rebuilt. Three boards, all with fixed geometry (`two-key-footswitch`'s smallest board, `macropad-6`'s 19.05mm key pitch, `pixel-badge`'s straight 8-pixel row) that could not move — only what the planner places *around* them could. **`two-key-footswitch` (fewest parts, rp2040-core's only dense neighbour is a debug header): 3 blocking → 0, `fab.ready` false → true.** rp2040-core moved +6.01mm in y (17.74 → 23.75, bit-for-bit `place_board()`'s own output for the composition), the debug header moved beside it instead of above it — no headroom left above the block under the wider gap. **`macropad-6` (rp2040-core has three dense neighbours: ldo-3v3, status-led, and the key field): 14 blocking → 10, still false.** **`pixel-badge` (rp2040-core boxed in by usb-c-data, two ldo-3v3s and a debug header on every side): 10 blocking → 9, still false.** Both remainders carry the exact signature ledger #53 already named: a via genuinely inside a pad on rp2040-core's own U3/U4 footprint — macropad-6's is `pcb_placement_error` "Via ... is inside SMD pad U3.RUN", plus `drc_violation` on a `RUN_SW` via and a `DVDD` via; pixel-badge's is `dfm_hole_clearance` "a pad passes 0.150mm from a via" on U3 plus a `U4` hole-clearance violation. Not spacing — #53 already traced macropad-6's `U3.RUN` finding to `R12` sitting 9.8mm from the pin it wires to, an escape route that has to cross the block's own decoupling row to get there. Two more boards hitting the same signature at the wider gap says the interior placement is the defect, not the number this ledger entry fixed. Neither board regressed (both were `fab.ready: false` before and after — the floor rule was never at risk) and pixel-badge's separate, unrelated `PX_18_DIN` floating-net defect (ledger #54) was fixed in the same pass, confirmed by `checks.floating_net_warnings` returning `[]` on the rebuild. **On the question ledger #52 leaves open — should 5.0mm become the planner's default rather than an override**: no. It already *is* the default everywhere a gap involving `rp2040-core` is computed, via `pair_gap()`; the override is scoped to the one block that measurably needs it. Every other block pair on all three boards routed fine at the 2mm floor — raising `BLOCK_GAP_MM` itself would widen every board in the fleet for a defect only `rp2040-core` has. | two-key-footswitch, macropad-6, pixel-badge (2026-08-17 rebuilds) | **Confirms #53 is still the right next fix, at Block level.** These three rebuilds are further proof the class lives inside `rp2040-core`'s own footprint (`R12`/`R13`/the decoupling row vs. their real pin locations), not in board-level spacing — nothing here moves the fix up the table. |
| 56 | **The via that has blocked four boards is one edge of one net's spanning tree, and it is unroutable by arithmetic — but every lever that removes it costs more elsewhere than it saves. Measured, did not land, reverted whole.** *What it actually is.* `DVDD` has three pins on the QFN-56 and they are not neighbours: `VREG_VOUT` (pin45) at local `(+1.8, +3.425)`, `DVDD1` (pin50) at `(-0.2, +3.425)` — same north row with **four foreign pads between them** (IOVDD1, USB_VDD, USB_DP, USB_DM) — and `DVDD2` (pin23) at `(+0.6, -3.425)`. tscircuit routes a net as a Euclidean MST over its pads, and with `C12` parked at `(3, -6)` the cheapest DVDD edge in the whole net was `VREG_VOUT -> DVDD1` at **2.00mm**. There is no legal way to make that hop: the pads are 0.2mm wide on 0.4mm pitch, so the bare gap between two of them is 0.2mm and a 0.15mm track needs 0.15 + 2 x 0.09 = **0.33mm**; the way round the outside is >5mm. So the router dives inside the pad ring — the 1.45mm annulus between the 3.1mm thermal pad (edge `|1.55|`) and the pad tips (`|2.99995|` = pad centre 3.42495 minus half the 0.85mm pad length, both read off a built circuit.json, not off the footprint's name). A 0.6mm via butted against that line sits at `|2.69995|`, leaving 2.99995 - (2.69995 + 0.15) = **0.150mm** of drill-to-copper against JLC's 0.20mm floor and **0.0008mm** of copper-to-copper against 0.09mm. Those are the exact numbers four sidecars report. *The fingerprint, and it is 4/4 and 0/8.* The edge leaves **two coincident vias** named `TR_U3_vregout` and `TR_U3_dvdd1` at one point. Scanned all twelve products: present on bench-i2c-scanner `(-8.232, 23.899)`, desk-air-monitor `(-8.674, 23.724)`, macropad-6 `(-12.774, 5.815)` and pixel-badge `(-11.733, -4.580)` — the four boards carrying the finding, and pixel-badge's `dfm_hole_clearance` names that exact coordinate — and absent from all eight that do not. *Four levers measured on the block's own gauntlet bench (baseline 0 blocking, `fab.ready: true`, 107 warn / 171 info, 106s).* **(1) The keepout is dead, and this is the load-bearing negative.** A four-rect annulus fencing 1.56..2.99 (inset 0.01mm off the thermal pad and off the pad tips so it touches no copper — `usb-c-power`'s rule), top layer only so the thermal pad's GND via can still escape underneath: **0 -> 45 blocking**, `pcb_autorouting_error` "ran out of iterations", 12 nets with no trace at all. Re-run at `10x`: **identical 45**, 516s against 287s. The obstacle branch ledger #45 found does work — the router honours the fence exactly — but the QFN's escape *genuinely uses* the interior, and a keepout cannot express "no via here" without also saying "no track here": every pad-to-via hop is radial, so any fence between a pad and its via kills the connection too. **(2) Wider via geometry:** `minViaPadDiameter` 0.6 -> 0.75mm, **0 -> 2** (`GND` via clearance). **(3) The block carrying its own DRC tolerances:** `routingTolerances` live on `subcircuitGroupProps`, so the block's `<group>` has to become a `subcircuit` to hold them — **0 -> 29**, the block routes in isolation and lands tracks through its own vias. **(4) `pcbRouteHints`:** not applicable — the failing edge is an MST edge the compiler invents, not a `<trace>` anyone declared. *One real but insufficient finding.* `minViaEdgeToPadEdgeClearance` **is** honoured and is free: the bench's worst different-net via-copper-to-pad gap goes 0.115 -> **0.165mm** and drill-to-pad 0.265 -> **0.315mm** at 0 blocking, same 107/171, same 104s. It is the router's normal margin that moves; the pathological via ignores it. bench-i2c-scanner rebuilt with `minViaEdgeToPadEdgeClearance="0.15mm"` came back **bit-for-bit the same 3 findings at the same coordinates**. Worth adopting as hardening; it is not this fix. *The placement fix works on the mechanism and loses on the board.* Moving `C12`'s pin1 to local `(0.8, 4.8)` — 1.700mm from both `VREG_VOUT` and `DVDD1`, so both beat 2.00 and the direct edge drops out as a cycle; 4.8 is the floor, U3's courtyard is 4.2mm half-height and an 0402's is 0.47mm — **does remove the coincident pair every time**. But the block has no slack, and the freed budget is immediately spent somewhere else: `C12` alone (C13 left at `(6,-6)`) gave bench **0 -> 5** (a via inside `U3.TESTEN`) while bench-i2c-scanner went 3 -> 3 with **the whole U3 signature gone**, replaced by a `sw-tact`/`GndPour` short and two GND-track findings; `C12` plus `C13 -> (3, -6)` gave bench **0 -> 0** (`fab.ready: true`, 106 warn / 172 info, vias 60 -> 48, pair gone) and bench-i2c-scanner **3 -> 14**. Same block, two configs one 3mm apart, opposite verdicts on bench and board — ledger #28's non-monotonicity, and hard proof that **the golden-blocks bench is not a proxy for this defect in either direction**. Nothing landed; `packages/golden-blocks` is untouched. | bench-i2c-scanner, desk-air-monitor, macropad-6, pixel-badge (sidecars, 2026-08-17); rp2040-core's own gauntlet bench x8 builds | **Block, and the next lever is a DVDD plane rather than a placement lottery.** The three DVDD pins should not be connected by tracks at all — the reference design joins them on copper. `<copperpour>` exists and the compiler already has `planeTerminatedSourceTraceLayers`, so a bottom-side DVDD pour under the QFN would terminate all three pins with one via each and delete the MST edge outright instead of out-bidding it. Two things any next attempt must budget for, both measured here: a real product build is the only valid gate (10 minutes for bench-i2c-scanner, against 2 for the bench that says nothing), and every candidate has to be measured on **more than one** board before it is believed. |
| 11 | A via whose center is *genuinely* inside an SMD pad — `C8.pin2`, 2026-08-11: via at (-26.55, -28.14) inside a 0402 pad centred (-26.51, -28.00). **This is not what blocked hydrate-coaster; that was #33's false positive arriving as the same kind, which is why the entry sat here for days without the real class moving.** Measured 2026-08-16 on the current board: 108 vias against 174 pads, zero containments — nothing is failing today, and the class is still only detected. | hydrate-coaster | **Pipeline or planner.** If the router does this unprompted it will do it on user boards. A real containment is a deterministic geometric repair like the via-bridge (#9's sibling in `kicad_normalize`), but it must validate the new position against every pad, track, via and pour on both layers first: an attempt that checked pads only produced six shorts, V3_3 to GND among them. |
| 27 | **The router-effort escalation cannot see the evidence it exists for.** Stage 0b decides from circuit.json, but the findings that say "route it differently" — `[clearance]`, `[shorting_items]`, `[hole_clearance]` — are KiCad's, three stages later. An rp2040-core board came back with five blocking findings, all five of them KiCad's, and the retry never fired. Every board now declares its own effort (#21), which is a floor a human chose rather than a rescue the pipeline performs. | rp2040-core bench; all three boards | **Pipeline.** Either run the KiCad cross-check before the escalation gate (it costs seconds, the retry costs minutes, and the fab costs two weeks) or move enough of its verdict into stage 4a that circuit.json alone can say "the route is wrong". #22 moved one — via drill clearance — and it now fires in time; `[clearance]` and `[shorting_items]` are still only KiCad's to see. |
| 12 | USB pair skew 18.88mm against a 3.8mm budget — and the EE confirmed the class (review 2026-08-15, finding 3): D+/D− must *travel as a pair*, which is geometry, not just length. **Now routed, not only reported, on the boards where a corridor exists** (#38). Still open because two of three boards have no corridor: the pair has to be routed *before* everything else for that, and the toolchain will not allow it. | terminal-keyboard, harness-puck | **v2 route stage** — a critical-nets pass that routes the pair *first*, so the corridor exists rather than has to be found afterwards; the planner-side length constraint remains `circuitlib`'s half. |
| 49 | **Two silkscreen labels printed on top of each other, and nothing in the pipeline could see it.** An engineer building `usb-c-breakout` (2026-08-17) shipped a first build whose pad legends read **"VBUSGND"** — the 5V and GND labels fused into one word — and the only thing that caught it was looking at the review PNG. Their words: silkscreen collisions "have no static check — only a full rebuild + eyeballing the PNG catches them". Every other silkscreen rule we have is about *ink on copper* (`gerber_silk_over_pad`, ledger #16) or stroke width (#9); nothing compared one label against another. A board can be electrically perfect and useless at the bench because the pad you need is the one whose name ran into its neighbour's, and "read every string in the render" is not a check, it is a hope. | usb-c-breakout, and on the shipped boards: terminal-keyboard **50 pairs** (every keycap legend under its own refdes), hydrate-coaster **3** (the debug-port labels running into a note) | **Check.** `checks.silk_overlap_warnings` boxes each label from its anchor, `font_size` and `anchor_alignment` and reports real overlaps, on the same layer, unrotated. The glyph model deliberately under-estimates (0.6x advance, 0.72x cap height, both measured against plotted extents) because a check that cries wolf about legible silkscreen is one people switch off, and a rotated label is skipped rather than approximated. Runs in the build's stage-1 scan, in the sub-second gate and in the pre-flight, so it costs nothing and lands before the picture does. |
| 51 | **The router escalation was unreachable for every board that followed our own instructions, and blind to the substrate that reports the defect.** Two independent faults in one ladder. (a) *A declared effort switched it off.* `_set_autorouter_effort` returned False whenever the source already carried `autorouterEffortLevel`, documented as "their choice wins" — and SKILL.md tells every engineer to declare `"5x"` on every board, so the retry never fired on a single product in the fleet. Three engineers rebuilt at 10x by hand, one at a time, guessing, while the mechanism built to do exactly that sat disabled by the instruction that was supposed to make boards route. An engineer building `two-key-footswitch` (2026-08-17) reported it as their worst friction and diagnosed it as the finding kinds; the kinds were right — `dfm_hole_clearance` has been in `ROUTING_ERROR_KINDS` since 08-11 — the declaration was what stopped it. (b) *The escalation cannot see KiCad.* It decides off `_scan(circuit_json)`, and `drc_violation` — clearance, shorting, hole-clearance, the kinds that actually block the RP2040 boards — is produced by the KiCad cross-check ~400 lines later in the same function. `ROUTING_DRC_TYPES` was added on 08-11 for precisely this case and is **dead at that call site**: at the moment the retry is chosen, no warning of that kind exists. Adding the set fixed nothing and nobody re-measured, because the board it was added for was fixed another way the same day. | two-key-footswitch, macropad-6, pixel-badge, and every fleet board that declares an effort | **Pipeline.** (a) is fixed: a declared effort is now a **floor**, `EFFORT_LADDER` climbs one rung from whatever the board asks (`5x` → `10x`; 100x is not a rung — measured at 28 minutes with no verdict, which is a hang), the prop is replaced rather than refused, and a board already at the top gets a finding saying so instead of silence. A retry that ran and did not help now says that too, in the sidecar, so the next engineer does not repeat it by hand. (b) is open and is the larger job: the escalation needs the second substrate's verdict before it decides, which means either a cheap DRC probe on attempt 1 ahead of the decision, or a retry that wraps more of the pipeline than the compile. Until then, a board whose only blockers are DRC ones must declare its own `"10x"` — which SKILL.md now says in those words, replacing a claim that the escalation "cannot rescue it" without saying it would not even try. |
| 48 | **`scripts/check` is documented as the cheap pre-flight and costs what a full build costs.** SKILL.md says *"No kicad, no fab export, artifacts discarded. Use before paying for a full run"*, and the script's own docstring admits the deviation: circuitpy exposes no stages-limited entry point, so it calls the same `build_board` into a tempdir and strips the path fields plus two packet-only warning kinds. An engineer building `rgb-lamp-controller` (2026-08-17) reported it as their third-worst friction: **194 warnings / 394 info from `check`, a nearly identical set to the real build's sidecar**, after a nearly identical wait. A gate that costs what the thing it gates costs is not a gate. | every board built through the skill | **Skill + pipeline.** Short term the doc now says what it does. The fix is a stages-limited entry point: compile (stage 0) then `circuitpy.fastcheck` over the produced artifact — the element scan, schematic truth, IoU, DFM and `@tscircuit/checks`, which is ~0.9s on our densest board — and no KiCad leg, no repair passes, no fab export. `fastcheck` already exists and is already sub-second; what is missing is a compile-only call it can be pointed at, since `build_board`'s compile lives inside a 1,500-line function. Until then the honest fast path is `python -m circuitpy.fastcheck <project>` against an already-built board, which SKILL.md now documents beside `check`. |
| 47 | **`dfm_power_trace_width` was unsatisfiable by construction, so it fired on every board forever.** The check took the *minimum* width over a rail's whole route, and the shipped autorouter **tapers into a terminal pad narrower than the track** (`getTerminalPadWidthLimit` / `getTaperWidthAtDistance`, read in the bundle) — so on any board whose pads are 0.2mm, which is every 0402 and every QFN, the minimum can never reach the 0.5mm floor. Measured 2026-08-17 the expensive way: V3_3 declared at its own measured 0.4mm ceiling (`circuitpy.netwidth`) and terminal-keyboard still reported *routed at 0.2125mm* — the taper, not the rail. Nobody could have cleared this finding by any means, and a check nobody can action is one people learn to scroll past — which is the state ledger #31 has been in since 2026-08-15. | all three, and every fleet board | **Check.** The rail's *run* is now measured away from its terminals (points within `TAPER_WINDOW_MM` = 0.6mm of either route end are the taper, which is a ladder of several points rather than one), and the message names both numbers: "V5 runs at 0.2mm ... (it necks to 0.15mm at its pads, which is the router tapering into a pad narrower than the track — that part is not something a width can fix)". The finding is now true, actionable, and clears when a rail is genuinely widened. What it does *not* do is tell you whether widening is possible — `netwidth.net_width_report` measures that ceiling per net, and the IDE shows it beside the box that sets the width. |
| 31 | **Power nets routed at signal width.** 5V/3V3 at 0.2mm — legal by current maths on this board, wrong as a class. Per-net width from current exists (ledger #13) but nothing distinguished a power net's *class* from a signal's. **Now widened, not only reported** — `circuitpy.powerwidth` at stage 0d takes the gap the autorouter already left, segment by segment, and never moves a trace: for each piece of copper the widest legal width is exact and needs no search, `2 × min(distance to obstacle − its own clearance)`, capped at the profile's power floor. Measured 2026-08-16 on the three boards, fraction of each rail's run reaching 0.5mm: terminal-keyboard GND 48% / V3_3 19% / V5 47%; harness-puck 52 / 25 / 64 / 44%; hydrate-coaster 46 / 27 / 50%. **Still open because the narrowest point does not move** — every rail still necks to signal width somewhere, so `dfm_power_trace_width`, which reports the minimum, still fires on every board. Voltage drop is an integral over the whole path and half a rail at 2.5× the copper is a real reduction, but it is not what the EE asked for. Two things the pass had to learn, both worth keeping: **a route point's width is shared by the two segments meeting at it**, so a 20mm run that clears everything still gets pulled to whatever its neighbour managed — every long segment is therefore cut into ≤1mm pieces so a neck can only spread one piece, and the invented boundaries are merged back where the widths match so the artifact does not bloat; and **a widener must never narrow**, which is not automatic — the first version let a piece take its computed allowance even when that was below the copper already there, and GND came back at **0.05mm, under the 0.1mm fab minimum**, i.e. a scrap board. Every piece is now floored at the width it started with. **2026-08-16: the router *can* be told, and told naively it scraps the board.** `<trace thickness="…">` writes `source_trace.min_trace_thickness`, and `getSimpleRouteJsonFromCircuitJson` gives a net-shaped connection `nominalTraceWidth = max(min_trace_thickness)` over its traces, which the router's own `TraceWidthSolver` holds while it searches — stepping down through `[nominal, (nominal+min)/2]` where the clearance will not take it and tapering into a terminal pad narrower than the trace. One declaration anywhere on a net sets the whole net. Built end to end on harness-puck (`work/railwidth/ab.py`, both sides parts-engine-off): **V5's narrowest point moved 0.150 → 0.500mm and its `dfm_power_trace_width` warning disappeared** — the first time this finding has been closed for any rail — V3_3_LED went 0.150 → 0.325mm, and the board went **`fab.ready` true → false, 0 blocking → 33**, including *Items shorting two nets (V3_3 and SWCLK)*, nine clearance violations at 0.015–0.084mm against 0.090mm and three hole clearances at 0.066–0.190mm against 0.200mm, **every one of them inside U3's fanout**. Reverted whole. **And the placement forbids the rest outright:** `work/railwidth/escape.py` fans 180 directions out of every rail pad against the placement only, and on all three boards the *only* pads that cannot carry 0.5mm are RP2040 QFN pins — 8 of V3_3's 25 on each board, 1 of hydrate's 34 GND pads — each measuring **exactly 0.4000mm**, which is `2 × (0.400 pitch − 0.100 pad half-width − 0.100 clearance)`. A 0.5mm rail needs 0.450mm of pitch; the part has 0.400mm. V5 (worst escape 1.1mm) and V3_3_LED (1.3mm) have no placement limit at all and miss the floor only because nobody told the router. Two artifacts in our own obstacle model came out with it, both recorded and neither changed: a rail is measured against **its own vias' drills** (a via's pad carries its net, its drill does not — ledger #24 one element out) and a **pour blanks its whole layer** (built from `brep_shape.outer_ring`, inner rings never read), which between them make harness-puck's V3_3 unwidenable on the bottom layer by construction. Full workings: `docs/architecture/rail-width.md`. | terminal-keyboard, harness-puck, EE review 2026-08-15 | **Board or planner, per net, from the measured escape** — not the block: the width resolves as a `max` over every trace on a net, so a block handed a rail by name cannot see that rail's other consumers, and marking part of a shared net is identical to marking all of it. `circuitlib`'s planner can run the escape measurement over the placement it just produced and declare the width the geometry permits (V5 and V3_3_LED at 0.5mm; V3_3 at its measured 0.400mm ceiling, which is unbuilt and is the next experiment). The floor lives in one place either way (`warn_power_trace_mm`); poured ground is exempt from both the check and the pass, because the plane is the current path. **2026-08-17: done per board across the fleet, with the per-net before → after table — V5 closed at 0.500mm on seven boards, V3_3 at its measured 0.4000mm ceiling graded and rejected, `i2c-sensor-hub` reverted whole twice. See "Ledger #31 on the fleet" below.** |
| 45 | **The pair pass shipped believing the corridor could not be reserved, and the corridor can be reserved.** `diffpair.py` refuses on two of three boards for the same reason — no channel wide enough survives the autorouter — and its docstring explained that nothing could be laid down first, because `getSimpleRouteJsonFromCircuitJson` builds from components, pads, plated holes, holes, vias and cutouts, with `pcb_trace` absent. The `pcb_trace` half is true and load-bearing. The conclusion drawn from it is not: that list is a *pre-filter*, and the obstacle builder itself has an explicit `element.type === "pcb_keepout"` branch pushing an oval or rect with `connectedTo: []` (read in the shipped bundle, 2026-08-16). **A keepout is an obstacle the autorouter honours, and it is not copper**, so a reserved channel costs nothing in the output and nothing has to be deleted from it. The pair's endpoints — a USB-C receptacle and an MCU — are fixed at *placement* time, before any routing, so the reservation needs no extra build either. | harness-puck, terminal-keyboard | **v2 route stage, and now it is one element rather than a research problem.** Emit a keepout corridor between the pair's two ends before the autorouter runs; stage 0c drops the reservation and lays the pair in a channel that is clear by construction instead of hunting one in the leftovers. The claim in `diffpair.py` is corrected in place so the next reader does not re-inherit it. |
| 46 | **Locking the parts flips a fab-ready board, and the loser is the USB-C receptacle.** Ledger #36 asks for a `parts.json` on terminal-keyboard and harness-puck so a build stops depending on how busy someone else's API was. `parts-book` writes one offline from the block defaults in seconds, and the BOM's blank Footprint column fills in immediately — harness-puck's rebuild went from **9 of 18 lines blank to 1**, and the extended-part fee is finally named per line. But the same rebuild went **`fab.ready` true → false, 0 blocking → 16**: four `gerber_drill_missing` (0.80mm holes with no drill hit) and twelve `gerber_pad_missing`, every one of them on **J1** — the `TYPE-C-31-M-12` receptacle, whose BOM line reads `SMD+TH hybrid` — and on **U4**, the flash. The design has the holes; the packet does not. So pinning a number changes the *footprint the board is built from*, and for a hybrid SMD/through-hole part the through-hole half does not survive into the plot. Reverted whole under the floor rule (a board that was ready may never come back not-ready), and the two boards still have no lock. Measured 2026-08-16, `CIRCUIT_FORCE_REGEN=1`, the lock written by `skills/parts-book/scripts/parts` with no `--lookup`. | harness-puck | **Pipeline / parts engine.** Find where a locked LCSC number replaces a block's own footprint and why the replacement loses the plated holes — the block's footprint plots correctly today, so the two are not the same geometry. Until that is understood, a lock cannot ship on any board carrying the USB-C receptacle, which is all three. The BOM half of the fix is already in and needs no lock to be correct on a board that has one (hydrate-coaster: 7 blank → 0). |
| 36 | **A board with no parts lock is not reproducible, and it ships anyway.** terminal-keyboard and harness-puck carry no `parts.json`, so every part is fetched from the supplier catalogue at build time. Two builds minutes apart took **47 and 17 `HTTP 429`** rate-limit refusals; each refusal is one part that silently loses its supplier footprint, and the build produces a packet regardless. The determinism symptom is the small half — the real one is that a fab-ready verdict can depend on how busy someone else's API was. | terminal-keyboard, harness-puck | **Parts lock (`parts-book`)** — pin every part to an exact LCSC number in `parts.json`, the way hydrate-coaster pins its 19 (zero round trips, byte-identical builds). Until then the harness reports it: `run_repeat.py` counts unresolved parts and 429s per run and names the network when the files differ for that reason. |
| 37 | **The drill-truth check does not know about our own via-bridge repair, and the repair does not honour the board's declared via rules.** `gerber_drill_extra` compares the drill file against `circuit.json`, but `kicad_normalize._fix_dead_end_vias` adds vias *after* circuit.json is written, so every board the bridge fires on reports drill hits "with no matching hole in the design — something was added between the design and the plot". Measured on terminal-keyboard at 100×90, twice: 228 drill hits against 225 designed features (213 vias + 8 holes + 4 plated), and the pipeline's own `kicad_normalized` line says "3 missing via(s) added under a B.Cu dead-end at a top-only pad" — the same three, at design (1.82, −28.77), (−19.87, −38.40) and (99.54, −136.65) in page space. The 112×90 board plotted 214 against 214 and bridged nothing, which is why nobody had met this. Two costs: a permanent false alarm on any board the bridge repairs, and no check at all that the extra hits *are* the bridge's. **And the vias it adds ignore the board's own declaration** — the source sets `minViaHoleDiameter="0.3mm"` and the packet carries a 0.15mm and a 0.20mm drill, because the bridge picks from its own size ladder. Both are legal at JLC (`min_via_drill_mm` 0.15) but neither is what the board asked for. | terminal-keyboard 100×90, 2026-08-16 | **Pipeline.** The repair already counts what it added (`Normalization.vias_bridged`); the drill-truth comparison has to be handed the post-repair hole set, or the repair has to write its vias back into the artifact of record. Its size ladder should start from the board's declared via geometry rather than a fixed list. |
| 39 | **A golden block moved its geometry and the board composing it was never rebuilt — so the packet in the repo says `fab.ready: true` for a board that today routes nothing at all.** The EE-review fix commits (4d9cec8, 3ce7e1c, 2026-08-15) re-vendored `rp2040-core` with the crystal cluster relocated from `pcbX=-8, pcbY=0` to `pcbX=0, pcbY=-10.5`. harness-puck composes that block at `pcbX=-2, pcbY=11`, which puts Y1's centre at (−2, +0.50) with a 3.6 × 2.9mm body against SW1 at (−2, −3.00) with a 7 × 4.4498mm body: the bodies overlap by 0.175mm and the courtyards by 0.900mm. tscircuit skips autorouting for the **whole board** on any placement error, so a rebuild from committed source produces **0 `pcb_trace` and 0 `pcb_via`** and 137 blocking findings (2 courtyard, 1 autorouting, 38 port-not-connected, 39 trace-missing, 1 dfa, 56 KiCad `unconnected_items`) — every one of them downstream of the single overlap. The committed packet was built 2026-08-13, before the move, and still claims 0 errors. Nothing gated it: the gauntlet builds each block's own testbench, the composition matrix builds block *pairs*, and neither builds the three examples. This is lesson D one level up — the ruler never looked at the thing that changed. | harness-puck, 2026-08-16 | **Gauntlet / CI** — a golden-block edit must rebuild every board that composes it before it can land, and a board whose packet is older than any of its inputs must not be publishable. The board-side lever is Dee's: SW1's position is product geometry (the key cap presses it), so the MCU cluster is what moves, not the button. |
| 40 | **`rp2040-core`'s own golden bench has been failing since 2026-08-11 and no run looked.** `test_bench_builds_clean[rp2040-core]` requires a golden bench to be warning-free; it reports `pcb_trace_too_long_warning — PCB trace is 12.46mm long, exceeding the 10mm maximum`. The geometry says which trace: the crystal cluster sits at `pcbY=-10.5` and the XOUT series resistor R11 at `pcbX=6`, so U3.XOUT → R11.pin1 spans √(6² + 10.5²) ≈ 12.1mm. It arrived with f5c6e54 (08-11 01:08, "Unblock ground planes and make RP2040 boards routable"), which is *after* the test landed (3e807e6, 08-10 22:50) — five days red. The same rule is violated on the boards, as a warning: terminal-keyboard **13.96mm**, hydrate-coaster **14.84mm**, both of which still route. That is worth stating precisely because ledger #6 says tscircuit "SKIPS AUTOROUTING FOR THE WHOLE BOARD" when the 10mm crystal maximum cannot be met — measured today it warns and routes, so #6's claim is either net-specific or overstated, and the block's own comment ("every endpoint within 9.1mm of XIN") is not true of XOUT. | rp2040-core bench; terminal-keyboard, hydrate-coaster | **Block** — put R11 inside the 10mm radius of XOUT rather than beside the crystal, and re-measure the route (#28 method: quote the lever with its blocking count). Until it moves, the suite that is supposed to make the library trustworthy is red, which is worse than the trace. |
| 42 | **A build does not use the checks in the tree, and nothing says so.** Two separate mechanisms, both of which hand back a packet with a fresh mtime and a verdict from a ruler that no longer exists. (a) *The cache does not know the ruler changed.* `_unchanged_prior_result` keys the idempotent short-circuit on the board's source fingerprint and the toolchain block — not on the check set — so fixing a check leaves every existing packet claiming what it earned under the old one, and a rebuild "correctly skips". The reverse is worse: a check that starts *catching* something re-judges nothing until a board's source happens to change. (b) *The pipeline that runs is a vendored copy.* `skills/circuitcode/scripts/packages/circuitpy/` is a generated copy of `packages/circuitpy/src/circuitpy/`, and it is what the CLI imports; `scripts/dev.sh` re-vendors on start but a direct build does not. Measured while closing #41: with the fix in the source and `CIRCUIT_FORCE_REGEN=1` set, a full 905-second rebuild returned **the identical blocking finding**, because the worker was running 2026-08-15's checks — the artifacts were fifteen minutes old and the verdict was a day old. Both are north-star's "every number carries the ruler it was measured with", from the side nobody watches. | terminal-keyboard, 2026-08-16 | **Pipeline** — put the check-set identity (the same hash the agent eval already prints) into the short-circuit key beside the source fingerprint and the toolchain block, and make the vendored runtime's identity part of it too, so a build either uses the tree's checks or refuses. Until then: `scripts/build/build-skill-runtimes.sh` then `CIRCUIT_FORCE_REGEN=1`, by hand, which is exactly the advice-a-user-has-to-remember this ledger exists to eliminate. |
| 16 | Raising silkscreen to the fab's 1.0mm floor (#9) made more of it land on pads: 70 strokes inside a mask opening became 99. Ink on a solderable surface stops the joint wetting, and JLCPCB clips it. | all three | **Block / layout.** The floor is not negotiable, so the text has to move: reference designators want a placement clear of the part's own pads, which is a footprint and `circuitlib.layout` question, not an exporter one. Created by a fix, and recorded rather than absorbed. |
| 54 | **A wire nobody drew leaves no element to be wrong about, and every check we owned before today only asked whether a connection that exists is correct.** `checks.floating_net_warnings` (2026-08-17) is the first thing in the stack that asks whether a connection that *should* exist is there at all, and on its first run over the fleet it found four instances of it on boards carrying `fab.ready: true`: `sensor-node-mini` composes `usb-c-power` under `rp2040-core`, so `USB_DP`/`USB_DM` reach only the MCU — a USB device that cannot enumerate, and the only way to flash it before its own SWD pads exist to use is the connector on the outside of the box; `desk-air-monitor`'s `SwTact` defaults `signal` to `BTN1` and nothing on the board ever traces a GPIO to that net, so the button is wired to itself; `pixel-badge` and `harness-puck` both name the last pixel's unused `DOUT` hop `PX_<n>_DIN`, indistinguishable by name from every real inter-pixel net, even though `ws2812-chain`'s own `BLOCK.md` already calls the dangling hop deliberate. All three passed the full gauntlet — compile, circuit.json scan, `@tscircuit/checks`, KiCad ERC/DRC, DFM, the review loop's schematic + PCB PNG read — because every trace on each board *was* correct; the missing one produced nothing for any of those to look at. None of the three needed a routed build or a fab plot to be caught: `floating_net_warnings` runs in the 17s pre-flight, off circuit.json alone. Three different catch points existed and none of them fired, one per defect: (1) *planner/composition* — nothing pairs `rp2040-core` with a data-carrying connector by default, or refuses `usb-c-power` unless the board separately declares a working flash path; a planner that models "does this MCU have a route a host can reach it by" would have refused the composition sensor-node-mini shipped with. (2) *block contract* — `SwTact`'s `signal` prop silently defaults to a net name (`BTN1`) that means nothing until a board traces a GPIO to it, and the block has no way to notice its own default went nowhere; a block whose output net is unconsumed is exactly the shape `sensor-bme280`'s `addr1` deviation-note pattern exists to flag, just never pointed at signal nets. (3) *review* — phase 3 of the silent review loop already reads `_schematic.png` and `_pcb.png` on every build and has done since the gauntlet existed; the prompt asks about craft, never "does every non-power net this schematic draws reach two parts" — the one question that would have caught all three defects (and the harness-puck instance specifically) without waiting for a check written months into the project. The check itself is the floor, not the ceiling: it is graded `warning`, not `error`, on purpose (a one-part net can be a testpoint or a reserved pin), so it still depends on someone reading the sidecar. `harness-puck`'s terminal net is fixed and rebuilt clean: renamed `NC_PX_18_DIN` — the check's own message names that escape hatch — same copper, `fab.ready` still true, 0 blocking. `sensor-node-mini` and `desk-air-monitor` are **not** fixed, and both attempts are the strongest evidence yet for ledger #52/#53: the correct board-level fix in each case (`usb-c-data` + `USB_DP`/`USB_DM` traces; `BTN_USER` traced to a GPIO) compiles and closes the floating-net finding, but rebuilding regresses `fab.ready` true → false every time, always the identical signature — via-in-pad and clearance violations clustered on `rp2040-core`'s own SWCLK/SWD/RUN/DVDD corner, the exact class #53 already calls open and non-monotonic to placement. Measured, not guessed: sensor-node-mini at declared `5x` came back 19 blocking, at `10x` (matching the effort every other `usb-c-data`+`rp2040-core` board in the fleet already declares) 11 blocking — real improvement, not convergence. desk-air-monitor, already at `10x`, went 0 → 38 blocking with `BTN_USER` on `GPIO3` (west edge of the QFN, ~33.5mm from the switch); moving the same net to `GPIO22` (east edge, ~27.6mm, physically clear of the SWD corner) cut it to 3 blocking, all three the same DVDD-decoupling clearance the board's own history already names as marginal at this effort; one pin further, `GPIO23`, came back *worse* — 12 blocking — confirming the router's response here is not monotonic in the way #53 measured on a different board. Every attempt was reverted whole under the floor rule; both boards ship today exactly as they did yesterday, still carrying the dead net, and `pixel-badge` is untouched. The planner/block/review gaps that let all four ship are still open, and now so is a fourth data point for #52/#53: the RP2040 interior's own escape routing is fragile enough that *closing this specific check's finding on these two boards may require #53's unsolved placement work first* — it is not simply a board-file edit the way harness-puck's was. | sensor-node-mini, desk-air-monitor, pixel-badge, harness-puck (`checks.floating_net_warnings`, 2026-08-17) | **Planner, block and review, in that order of leverage — check-only today.** Planner: a composition rule that an MCU block needs a reachable host path (data connector, or an explicit accepted-SWD-only flag) before it is buildable. Block: a signal-producing prop like `SwTact.signal` should be checkable for "nothing consumes this," the same shape as an unpaired block, not left to a net-name convention a board author has to remember. Review: add the one connectivity question to the phase-3 PNG read — cheap, since the model is already looking at the picture — rather than relying on a circuit.json scan nobody runs until a check exists to run it. |
---
## What these three boards taught about checks themselves
Not defects in a board — defects in the *way we look at boards*. Both were
caught before shipping, and both would have been invisible afterwards.
| # | Lesson | How it surfaced | What changed |
|---|---|---|---|
| A | **A check that cannot parse a shape silently passes it.** Our gerber reader could not evaluate parameterised aperture macros (`RotRect`, `RoundRect` — defined once, given their real numbers at each `%ADD%`), so it assigned them zero size and dropped them. **56 of 230 mask openings on harness-puck — a quarter of the layer, and every fine-pitch QFN pad — were never examined.** The sliver count went from "2" to the true 10 once they were resolved. | measuring a fix's before/after and finding the two numbers incomparable | macro parameters are substituted and rotation applied; nothing is dropped for being unparseable |
| B | **A fix can blind its own smoke alarm.** Setting `solder_mask_min_width` made KiCad plot the mask as 223 filled regions instead of 174 flashed apertures. The check only understood flashes, so it would have reported a clean mask on every board where the fix had been applied — quiet, not clean. | re-running the check on the fixed packet before believing the fix | the reader handles both representations, and the setting itself was **retracted**: measured against the same yardstick it removes no web at all, and costs an 8x slowdown. A change that alters the representation and not the board is a placebo |
| C | **A finding is a claim, and the first thing to do with it is measure it.** "Via inside SMD pad" was read as a router defect and answered with router work — effort levels, clearances, `allowViaInPad`, freerouting, a ground plane, and a nudge script that created six shorts. None of it could have worked: the via named by the finding was 0.3009mm outside the pad named by the finding, and no via on the board was inside any pad. Twenty lines of Python over the artifact of record said so in under a minute, and would have said so on day one. | measuring the two coordinates in the message against each other before touching the router | every via-in-pad claim is now re-measured against the geometry, and one whose own numbers disprove it is dropped with the reason recorded (#33) |
| E | **A repair pass graded by its own model is exactly as right as its model.** The differential-pair pass measured every clearance rule it knew — copper to copper, copper to drill, drill to drill, edge, keepout, pour — refused anything that broke one, passed its own unit tests, and turned a `fab.ready` board into a board with 14 blocking findings. The rule it did not know was the *cross* term: one via's pad against another via's drill, 0.196mm against a 0.20mm floor. Nobody would have written that rule down, because it only exists where two vias of one net sit at a pair's pitch. | running the whole pipeline on the pass's output instead of trusting the pass's own verdict | the pass takes a `grade` callback and the pipeline hands it the real gate; the rewrite is kept only when it adds no blocking finding the original did not have, and a grader that raises is a refusal. Every repair that writes copper should be held this way |
| F | **An absent value is a claim, and nothing checks a claim nobody makes.** Three defects on one day, one shape. The BOM's Footprint column was empty on every board ever shipped, because the catalog resolver pointed at a directory that has never existed and returning `{}` was documented as "the honest answer" (#43). The pair pass refused to reserve a corridor because a list it read was the wrong list, and an unexplored option leaves no trace at all (#45). And `schematic_symbol_short` fired on two buttons on all three boards for two days and was written off as noise, because one of the two artifacts it could have meant was clean (#44). Every check we have fires on a value that is *present and wrong*. None of them can fire on a column that is blank, an option nobody tried, or a warning somebody explained away — and those are exactly where a system that measures itself goes blind. | asking what each empty field, silent code path and dismissed warning would look like if it were broken, rather than what it would look like if it were fine | a missing catalog now warns with its consequence instead of blanking a column; the corrected obstacle reading is written into the file that got it wrong; and a check that fires on every board is treated as a finding until the *artifact the reviewer actually opens* is measured, not until one artifact is clean |
| D | **A determinism harness called an empty route reproducible.** The first thing `run_repeat.py` was pointed at was harness-puck, and it reported *route bytes IDENTICAL* over three runs. The board had routed nothing: a courtyard overlap (Y1 against SW1) makes tscircuit skip autorouting for the whole board, so circuit.json carried zero `pcb_trace` and zero `pcb_via`, and the sha256 of nothing is stable forever. It is lesson A one level up — the ruler could not see the shape it was measuring, so it passed it. | pointing the new ruler at a board before checking the board | the harness refuses to report on a run with no copper or a `pcb_autorouting_error`, prints the build's own messages, and exits non-zero; every report now carries the effort level, the copper count and the load average it was taken at |
| G | **One severity policy, two callers, and only one of them ran it.** `pcb_trace_too_long_warning` reaches the verdict twice: `harvest_circuit_json` reads the element out of circuit.json and grades it `warning` from its own suffix, and `run_tscircuit_checks` reports the identical finding out of `@tscircuit/checks` and grades everything it returns `error`. That is fine, because `generation._scan` ends with `checks.dedupe`, whose first-occurrence-wins keeps the harvested copy — the policy is not in either leg, it is in the *order* plus the dedupe. So a second caller that collected the same legs and skipped the dedupe got a different answer from the same artifact: the IDE's `board_fast_check` called **all three shipped boards `blocked`** while their sidecars said `fab.ready: true`, over two findings whose own kind ends in `_warning`. The tell was there in the word and nobody read it. | building a second consumer of the same check legs — the sub-second edit gate — and pointing it at a board the build had just called clean | the gate deduplicates with the build's own function rather than re-grading anything, because the ordering that decides which copy survives is the build's; and where a leg cannot run at all (no `product.json`) the gate reports the leg it lost instead of raising, since "the answer went away" is the one response a gate may not give |
| H | **A unit test proves a piece works; nothing proved the app reached it.** Five instances, four of them inside one day, all the same shape — code that compiles, passes its own tests, gets written down as landed, and is called by nothing. `board_fast_check`, the whole legality gate, with no client caller. `onPlacementRotate`, passed as a prop the component never declared. `PadHeader`, a real block that existed only under a name (`DebugPort`) no engineer would search for, so two of them hand-copied its internals. `editEngine.js`, 687 lines and zero importers. And `app_set_effort` — settings key, driver `--effort`, three phases of the review loop — while the composer's pill wrote to localStorage and appended a prompt directive, so the pill read "Max" and every turn ran at the CLI's default. Both sides of every one of those joins were green. The tell is that a *join* is the one thing a unit test cannot cover, because covering it is what integration means, and we kept writing the two units. | asking, after the third one in a day, what the first two had in common — and then asking the same question of the command table rather than of a memory | `viewer/src/server/circuit/commandsAreWired.test.mjs` reads the server's command table off `http.mjs` and asserts every name appears in shipping client code (tests excluded — a command called only by its own test is the bug), or sits in an `AGENT_ONLY` map **with a written reason**. Deliberately a string search, not a call graph: half the client reaches the API through `transport.ts` and half through a bare `fetch("/api/<cmd>")`, and a check that understood only one would be blind exactly where the last bug was. It found `app_set_effort` the first time it ran. The same question is still open one layer out — a *block* nothing composes, a *check* no gate calls — and wants the same kind of ruler. |
| I | **A fix that closes the loud half of a gap and leaves the quiet half is the same defect, one tier down — and the quiet half is worse, because nobody is looking there.** The fast edit gate could not run KiCad, so it reported a count that was structurally low; an engineer read `counts.error: 1` on a board whose real number was 3. The fix read the sidecar and reported what the gate is blind to. It shipped, it was tested, and it counted **`severity == "error"` only** — which is not where KiCad's findings mostly land. A panel judge measured the hole the same day: two-key-footswitch carries 157 warnings in its sidecar, 146 of them kinds the gate cannot produce, against a chip that disclosed nothing at all; terminal-keyboard hides 379 the same way. The same judge found that the set of kinds had been typed **from memory** — `gerber_silk_on_pad` for the real `gerber_silk_over_pad`, plus two kinds (`bom_line_unorderable`, `bom_no_supplier_part`) that appear nowhere in the codebase — so even inside the tier it did count, it under-counted. Both halves are the same mistake: fixing the instance that was reported instead of the class that produced it. | two-key-footswitch, terminal-keyboard, pixel-badge; found by round 4's Verdict judge, not by the author | **Two rules, both now enforced by tests rather than by intent.** (1) *A disclosure covers every tier the thing it discloses can appear in* — `last_build_verdict` counts blocking and advisory separately and both are on screen. (2) *A list of strings that must match strings written elsewhere is a typo waiting to under-report*, so the classifier is a prefix rule (`gerber_` is the plotter's whole output) plus a named rest, and a test walks **every sidecar in the repo** and fails if any kind the build actually emits is unrecognised — measured against the artifacts, never against memory. The general form is the one worth carrying: when a fix is prompted by one report, ask what the *other* instances of that shape are before shipping it, because the reporter only saw the one that happened to bite them. |
## Ledger #31 on the fleet: 5V closes, 3V3 does not, and one board takes neither
*2026-08-17. The finding is the EE's, 2026-08-15: "the main power nets, 5V and
3V3, are currently the same width as a signal, 0.2mm ... should be 0.5–1.0mm."
`docs/architecture/rail-width.md` had already established that the router takes
a per-net width and that handing it one blindly scraps a board (harness-puck:
`fab.ready` true → false, 0 → 33 blocking, two nets shorted). What was missing
was a fleet-wide answer, measured per board. Here it is.*
**The procedure, unchanged on every board.** Measure with
`python -m circuitpy.netwidth <project> --rails` against the *placement*, which
reports the widest track each net's own pads can escape at. Declare only what
that clears, with `<trace thickness>`, never rounded up. Rebuild. Read
`narrowest_mm` back out of `netwidth` and the blocking count out of the sidecar.
`fab.ready: true` and the starting blocking count are a floor: a board that
comes back worse is reverted whole, because a wider rail on a board that no
longer ships is not progress.
**Every rail on every board measures the same two ceilings**, which is the first
result and was not obvious: V5's escape is **1.10mm** on all nine boards, held
by the USB-C receptacle's own VBUS pad, and V3_3's is **0.4000mm** on all seven
RP2040 boards, held by an `IOVDD` pin — `2 × (0.400 pitch − 0.100 pad half-width
− 0.100 clearance)` to four decimals. The ceiling is a property of the parts,
not of the layout.
| board | net | ceiling | declared | narrowest before → after | blocking | `fab.ready` | sidecar warnings |
|---|---|---|---|---|---|---|---|
| `usb-c-breakout` | V5 | 1.10 | **0.5** | 0.200 → **0.500** | 0 → 0 | true → true | 49 → 48 |
| `dual-rail-psu` † | V5 | 1.10 | **0.5** | 0.200 → **0.500** | 0 → 0 | true → true | 54 |
| `dual-rail-psu` † | V3_3 | 1.30 | **0.5** | 0.250 → **0.500** | 0 → 0 | true → true | 54 |
| `two-key-footswitch` | V5 | 1.10 | **0.5** | 0.200 → 0.375 | 0 → 0 | true → true | 157 → 157 |
| `two-key-footswitch` | V3_3 | 0.40 | **0.4** | 0.200 → 0.225 | 0 → 0 | true → true | 157 → 157 |
| `dual-sensor-node` | V5 | 1.10 | **0.5** | 0.150 → **0.500** | 0 → 0 | true → true | 178 → 180 |
| `dual-sensor-node` | V3_3 | 0.40 | **0.4** | 0.150 → 0.175 | 0 → 0 | true → true | 178 → 180 |
| `env-logger-usb` | V5 | 1.10 | **0.5** | 0.150 → **0.500** | 0 → 0 | true → true | 172 → 171 |
| `env-logger-usb` | V3_3 | 0.40 | — | 0.150 → 0.150 | 0 → 0 | true → true | 172 → 171 |
| `rgb-lamp-controller` | V5 | 1.10 | **0.5** | 0.200 → **0.500** | 0 → 0 | true → true | 194 → 196 |
| `rgb-lamp-controller` | V3_3 | 0.40 | — | 0.200 → 0.200 | 0 → 0 | true → true | 194 → 196 |
| `sensor-node-mini` | V5 | 1.10 | **0.5** | 0.200 → **0.500** | 0 → 0 | true → true | 158 → 159 |
| `sensor-node-mini` | V3_3 | 0.40 | — | 0.200 → 0.200 | 0 → 0 | true → true | 158 → 159 |
| `desk-air-monitor` | V5 | 1.10 | **0.5** | 0.200 → **0.500** | 0 → 0 | true → true | 170 → 171 |
| `desk-air-monitor` | V3_3 | 0.40 | — | 0.200 → 0.200 | 0 → 0 | true → true | 170 → 171 |
| `i2c-sensor-hub` try 1 | V5 + V3_3 | 1.10 / 0.40 | 0.5 / 0.4 | 0.150 → 0.500 / 0.175 | **0 → 3** | **true → false** | **reverted** |
| `i2c-sensor-hub` try 2 | V5 only | 1.10 | 0.5 | 0.150 → 0.500 | **0 → 5** | **true → false** | **reverted** |
† `dual-rail-psu` is the no-MCU board that proved the loop earlier the same day;
its numbers are here because they are the only case where a *3V3* rail reached
the 0.5mm floor, and the reason is that it has no RP2040 to reach.
**Result 1 — V5 closes, and it is the rail that matters.** Seven boards now
carry their 5V at 0.500mm end to end, 2.5–3.3× the copper, and
`dfm_power_trace_width` is gone from V5 on every one of them. That is the EE's
finding closed for the rail that carries the whole board's current, on boards
that still ship.
**Result 2 — V3_3 at its own ceiling is not worth the risk, and now there are
numbers.** Declaring 0.4mm — the exact measured ceiling, the most the geometry
permits — moved the narrowest point by **0.025mm** on the two boards that
survived it, and on `i2c-sensor-hub` it produced *Items shorting two nets (V3_3
and GND)* on a 1.5mm track beside U3's fanout, plus a solder-mask bridge and an
accidental pad contact. The reason the gain is that small is the reason the
declaration is dangerous: the rail's worst point is *at* a QFN pin either way,
where the router tapers into a pad narrower than the trace, so the declaration
buys bulk copper the check never reports and cannot buy the number it is graded
on. **The four boards done after that failure declare V5 only** — which is what
`docs/architecture/rail-width.md` §5 said to do before any of this was built,
and the fleet now agrees with it.
**Result 3 — a pad-escape ceiling is not a routing guarantee, and
`i2c-sensor-hub` is the counter-example.** Its V5 pads escape at 1.10mm like
everyone else's, and V5 *reached* 0.500mm on the rebuild — but the wider rail
pushed a via into `J1.A6`'s SMD pad, 0.191mm from a drill that needs 0.2mm, with
two clearance violations at 0.041 and 0.059mm against 0.090mm. `netwidth`
measures what a net's own pads permit; it does not model the congestion the rest
of the board puts in the channel. Five blocking findings from the *safer* of the
two attempts is the honest shape of that gap. Reverted whole, twice, and left at
its committed `fab.ready: true`.
**One side effect, named rather than buried.** On three boards the re-route
turned `holes_co_located` findings into `hole_to_hole` ones at 0.100–0.158mm
against KiCad's 0.1995mm (`dual-sensor-node` 3 → 6, `rgb-lamp-controller` 3 → 1
plus 5). Same via clusters, near-miss instead of stacked, non-blocking in both
forms — the total sidecar warning count moved by −1 to +3 per board. It is a
real change in the artifact and it belongs in the record even though nothing
gates on it.
**Where the remaining fix belongs.** V3_3 will not reach 0.5mm on any RP2040
board by declaration, at any effort level — that is arithmetic, not routing. It
needs either a part whose pitch permits it, a fanout the board owns rather than
the router, or a poured V3_3 plane. Until one of those exists, `#31` stays open
for V3_3 and is closed for V5 on eight of the fleet's nine buildable rails.
| J | **A measurement shown without its scope is read as a guarantee, and people act on it.** Three separate defects in one day, one shape. `board_fast_check` printed `counts.error: 1` on a board whose real number was 3 — true about the one ruler it can run, read as the whole truth, and an engineer only caught it because they happened to diff it against the build. `netwidth` printed *can take 0.4mm* — true about what a net's own pads permit, silent about the congestion the rest of the board leaves in the channel — and `i2c-sensor-hub` was reverted **twice** for taking it as permission, once at three blocking findings including two nets shorted and once at five, both at widths under the printed ceiling. `fastcheck.py` promised a *sub-second* verdict — measured on a quiet machine, 1.1-1.3s at load average 15.5. Every one of the three numbers was honestly measured. Every one was stated in a way that answered a question nobody asked and stayed silent on the one they did. | two-key-footswitch, i2c-sensor-hub, terminal-keyboard — all three found by someone other than the author, within hours of the number shipping | **A number goes out with the boundary of what it measured, in the same breath, in the place it is read.** The gate reports both severity tiers and what it is structurally blind to; the width row says "its pads allow 0.4mm … not a promise the router can route it"; the latency claim carries the load average it was taken at. The general rule, which is cheaper than any of the three fixes: **before shipping a number, write the sentence that begins "this does not tell you…" — if that sentence would change what a reader does, it belongs on screen beside the number, not in a docstring.** Lesson C says a finding is a claim and must be measured; this is its other half — a measurement is also a claim, and its scope is part of it. |
## The rule
When a board fails and an agent fixes it, the question is never "is the board
fixed". It is **"why did the first composition get this wrong, and what stops
the next one"**. The fix belongs in the planner, the block, the skeleton or
`circuitlib` — never in advice a user has to remember, and never only in the
example that happened to catch it.
A block that needs correct handling to be safe is a block that will be handled
wrong.
e4d010b16This file is pinned to the selected release. Download the original to open it in your design tool.