An engineer’s pass over the five examples

This is a chronological record, not a claim that every earlier conclusion still holds. Section 26 corrects the motor package rating, bypass network, decoupling placement and the scope of multilayer return-path checks; it supersedes the corresponding claims and measurements in earlier rounds.

The five projects under examples/ were generated to be reviewed. This is the review: the five designs read the way an electrical engineer would read them — circuit theory first, then the physics of the layout, then whether the drawings can be read at all. Every finding ends in one of four places, and the point of writing them down is to say which:

1. Electrical

Found wrong and fixed

Judged right, and answered in the gate file

Open

Four of the five items that stood here were closed in round twenty (§25): the power inputs are fused and clamped, the buck’s output capacitor states its ESR, the FPGA’s clock leaves its oscillator through a series resistor, and the DAC’s mute is under the FPGA’s control. Three of the four became rules. What is still open:

2. Electromagnetics and layout physics

3. Schematic readability and semantics

4. Artwork readability

5. What changed in the toolkit because of this pass

finding disposition
connections by name, not wire rule readability.label_only + fixed (sheet router: wire trees, junction dots, mirrored connectors)
inverted LED, headers facing away, notes over parts, connector in the title block fixed — found by the router, not by a rule; the checkable parts became rules afterwards (below)
ground vias at the end of a track fixed on fpga-audio (vias anchored beside each capacitor’s ground pad)
a wire run through a junction — KiCad 9 connects one side only rule readability.wire_through_junction
two wires drawn along one line rule readability.overlapping_wires
a connector row facing away from its signals rule readability.facing_away
pins or notes on the frame strip or title block rule readability.margin_intrusion
a note printed over a symbol rule readability.text_over_symbol
what stays judgment — polarity semantics, wire-versus-label taste, router cost trade-offs the two authoring guides, kicad-schematic-authoring and kicad-pcb-authoring
floating AC-coupled node rule analog.no_dc_path + fixed (R6 on opamp-filter)
scenic-route routing rule route.detour
signals over plane cuts rule route.return_path (parser now keeps zone outline + fill)
escape necks damned as thin power track.thin_power re-judged on contiguous run vs power_neck_mm
decoupling asked of names, not pins analog.missing_decoupling now reads pin types; output-driven nets exempt
unused pins as defects net.single_pin honours no-connect flags
charge pump miswire, VCCPLL tie, missing CS pull-up, LDO reservoir fixed in fpga-audio/reviewed
package-geometry decoupling distance waived per project, reason in each gate.toml

6. Calibration

Every new or changed rule was run over the 18 human-drawn demo projects that ship with KiCad, before and after, because a rule that fires on human work is measuring something other than machine work:

rule on the demo corpus
readability.label_only 0 findings — human sheets draw their wires
analog.no_dc_path 0 findings
route.detour 8 boards at a 2.5x threshold, 0 at 4x — 4x is where human routing stops and machine tours begin, and is the shipped default
route.return_path 4 boards — real two-layer boards genuinely have this disease, which is why it is a warning and not an error
track.thin_power (re-judged) 9 boards before, 0 after — every one of the nine was a pad-entry or escape neck, which is exactly what the rule was wrong about
readability.wire_through_junction 0 findings — the editor splits wires at tees, so human files never contain one
readability.overlapping_wires 1 board, 7 spots — and they are real drawn-twice wires, not noise
readability.facing_away 6 boards — humans park edge connectors facing outward on purpose, so it is graded info; the ai-generated policy still blocks on it
readability.margin_intrusion 6 boards — mounting holes and logos live in the margins of human sheets, so info, same reasoning
readability.text_over_symbol 2 boards — info from the start: the text extent is estimated, not measured
readability.text_over_wire 10 boards, 942 findings — graded info for the same reason. A generated design still has to have none: the ai-generated policy promotes it to an error
readability.text_over_text 14 of the 18 boards, 4531 findings — also info. Human sheets are full of text a character-count estimate reads as touching, and a rule that fires that often on human work is measuring the estimate rather than the drawing. Under ai-generated it is still an error, and every reviewed/ sheet has zero
route.acute_angle (pad exemption re-cut) 8 boards / 127 corners → 9 boards / 205 on the same demo boards. A 61 % rise on a rule already graded info, in exchange for seeing 11 of the 13 real hairpins on our own five boards that the old disc hid. It was never quiet on human work, which is why it is info and only the ai-generated policy promotes it
route.wander 1 board of the 18 at the shipped 2.0x — the same order as route.detour at 4x, and the one it finds is a real out-and-back. Measured against a baseline that walks round whatever package the straight line crosses, so a feedback wrap is not counted as a tour

Every reviewed/ project now carries a gate.toml and passes eda gate --policy examples/<name>/gate.toml; every waiver in those files is one of the judgments above, stated as a reason a reviewer can disagree with. That is the intended shape of the mechanism: findings are either fixed, checked, or answered — never silently absent.

7. The reviewer’s pass, round two

The project owner reviewed the regenerated set and returned a list of conventions the drawings and the artwork still broke. Each item went the same three ways as before — into the generator, into a rule where a rule can check it, into the authoring guides where only judgment can:

item disposition
power symbols drawn sideways fixed (upright symbols, jog wires, per-connector ground/rail bus columns) + rule readability.power_symbol_orientation — graded info because twelve of eighteen human demo sheets rotate them, and the ai-generated policy promotes it anyway
PWR_FLAG parked in a labelled row fixed — flags are wired in beside the pin the rail actually comes from; placement is judgment, so the rest is the guide
ratings hidden in fields fixed — R/C/L print voltage, tolerance and power beside the part
capacitors drawn far from their IC, notes and fields colliding guide (kicad-schematic-authoring), with readability.text_over_symbol and margin_intrusion catching the checkable part; per-unit capacitor adjacency stays open
right-angle corners rule route.right_angle (info — thirteen of eighteen human demo boards corner at 90, and this generator’s own router still does; chamfering it is open)
connector pins unlabeled on silk fixed (net names beside every connector pad, outside the courtyard) + rule silk.unlabeled_connector
indicators unlabeled fixed — silk_label per part: “5V OK” beside the power LED
no board name or revision fixed (name + rev, bottom centre) + rule silk.missing_board_id
ground pour on one face only fixed — both faces poured, stitched by an automatic ring of edge vias + rule layout.pour_single_sided (context: a one-sided pour is a design choice real boards ship with)
high-current return left to the pour fixed on buck-5v — an explicit 1.0 mm ground return along the bottom edge, input terminal to output terminal
feedback path stretched verified rather than changed: the buck’s FB trace is Manhattan-minimal to the output capacitor it senses; the guide records the rule of thumb (short, and never parallel to SW)
clearance crowded without need, connectors not at the board edge guides — the router’s crowding cost and the connector-placement convention are stated there; neither is measurable enough to be a rule yet

8. The reviewer’s pass, round three

The owner read the round-two set and held it to a stricter standard: the schematic is judged from its plot, as the original an artwork is checked against — so structures that only make sense as data (an S-expression’s label table, a rail assembled from six power symbols) are out wherever the drawing can carry the meaning instead. The artwork adds manufacturing and performance: no wasted copper, no accidental angles.

item disposition
IC surroundings drawn as labels fixed — the buck’s converter loop is all wire now: wired_power draws the +12 V and +5 V rails as horizontal lines with taps, and the FB wire visibly returns from the output rail to the pin
capacitors connected by symbol, not wire fixed — rail caps tap the drawn rail in board order, bulk to bypass
indicator blocks mixed into the power run fixed — LED blocks sit apart with their own note on every design
notes piled in one corner fixed — note_blocks anchors each note beside the circuit it explains, on all five sheets
labels on the circuit side of a net fixed — the surviving label prefers the connector pin (J*), and label text reads away from its pin so it cannot merge with the pin number
miscellaneous logic latticing the sheet fixed — label_nets keeps the motor’s control lines and the Pico’s forty-pin map as names at both ends; the readability.label_only waiver in each gate.toml is where that convention is argued
VM rail wandering fixed — the motor’s VM runs J1 → bulk → bypass on one line, rises once, and crosses to the pin with the charge-pump cap tapping the riser
test points for sim-vs-measurement fixed on opamp-filter — TP1-TP3 on filter input, output and VREF, wired on sheet and board; test.no_testpoints already notices their absence
screw terminals facing along the board fixed — J1/J2 entries point off the board edge on buck-5v
pour cut out around connectors for no reason fixed — the buck pours edge to edge; the pico’s stripes under the module are forty foreign through-holes at 2.54 mm, and the sheet now says so
TO-263 tab treated as just a pad fixed — a seven-via ring beside the tab (never on it: via-in-pad drinks solder) ties it into both planes as heatsink and return
board silk missing author fixed — name, rev and author line on every board
90-degree corners in tracks fixed — every square corner chamfers to two 45s unless a via needs the square one (_chamfer_tracks)
shallow fan angles (20-30 deg) fixed — escape fans bend at staggered 45s (fan), and the new rule route.odd_angle reports corners off the 45 grid (info: eleven of eighteen human demo boards bend off-grid somewhere)
widening a track mid-run fixed — motor outputs leave the pin field at the row’s full width and widen once, at the field’s edge; the guide states the principle
J2/J3 colliding when mated fixed — the opamp’s power header moved to the top-left edge, clear of the output header
return path through the front face centre improved — four mid-board ground vias give the sliced front pour short ways to the plane on opamp-filter
routing under digital ICs rule of thumb in the guide + a route_keepout mechanism in the generator; on fpga-audio even the codec’s underside turned out to be load-bearing corridor - with it closed the ground drops beside it lose their last lane - which is the unavoidable case the guide names
GPIO/programming header interior judged — two routing attempts at the bottom edge found no lane: that edge is the south escape fan’s corridor, so the debug header keeps the one interior window that routes, and the guide states the trade
capacitors with no visible owner fixed — fpga bank caps sit beside the bank unit they feed, codec caps beside the codec, each group with its note
I2S weave between FPGA and codec fixed — the bus is names at both ends now, and the sheet reads as the pin map
feed diode backwards found by this pass: the pico’s D1 had cathode on the external 5 V - the supply could never reach VSYS; polarity fixed in the netlist

9. The reviewer’s pass, round four

The buck passed. The other four came back with artwork faults, and one sentence recurred across three of them: do not let the ground pour be cut up. Three rounds had asked for it in different words and nothing in the toolkit could say whether it had happened, so this round starts by building the measurement.

Two rules, and what measuring taught us. layout.pour_fragmented reads the share of a pour’s copper in its largest connected island — the case where the plane is genuinely in pieces. layout.pour_coverage reads how much of its own outline a pour actually filled, which is the number the eye takes off the plot. Both raster the fill rather than adding polygon areas, because a generated fill is hundreds of overlapping rectangles and their areas cannot be added.

Measuring immediately explained the plots. In the dense half of a layout the clearance channels shred the pour into fragments, and a fragment touching no ground pad of its own is dropped by the filler as an orphan — that is where the blank fields come from, not from the tracks themselves. Stitching the interior on a six millimetre mesh, not just the rim, gives every fragment an anchor; the motor driver went from 63 % of its outline to 76 % on that change alone.

Measuring also corrected the rule. Compacting the filter — 80 × 45 mm down to 58 × 42, every run shorter, which is exactly what the review asked for — made its coverage fall, because the same copper in less area is a smaller share of it. A number that drops when the artwork improves cannot be a verdict, so coverage reports and pour_fragmented faults.

item disposition
motor: ground pour fragmented fixed — interior stitching, and the output bundle drawn at 1.4 mm instead of 2.0: two tracks that far apart leave 0.3 mm between them, under the filler’s sliver limit, so the strip vanishes and the pair reads as one wide hole
motor: track width changed mid-run fixed — the escape is short enough that the narrow section is a pad neck (inside track.thin_power’s allowance) and the widening happens once, where the package stops constraining it
motor: 45s bent too early, right angles, doubled-back runs fixed — every escape gets a three millimetre straight before its first bend, and the board lost the twelve millimetres of open field the router was detouring across
pico: LED circuit overlapping fixed — the indicator has its own column and its note moved with it
opamp: analog and power traces routed carelessly fixed — the board is redrawn around its nets: the half-rail buffer sits with the two parts that use its output instead of across the board from them, so VREF no longer runs corner to corner, and the supply leaves the header as two short branches
opamp: placement and routing that fragment the front pour improved — total routed length 412 mm on a board 40 % smaller in area; the remaining crossing of the back plane’s cut is waived in numbers (ten nanohenries at a kilohertz)
fpga: parts too far apart, wiring cluttered fixed — the sheet gathers into blocks (power, clock, config, codec), each with its note beside it, in place of parts spread over the whole of an A3
fpga: bends off 45, pour fragmented, routing under the codec, analog return under digital, bypass caps away from the regulator partly open — the QFN’s four-sided 0.5 mm escape is the constraint behind most of them, and two attempts at closing the codec’s underside left the ground drops beside it with no lane. What is fixed is fixed; what is not is stated with the reason rather than quietly waived
calibration on the thirteen KiCad demo boards whose zones parse, layout.pour_fragmented fires once and layout.pour_coverage on eight — which is the split the two are meant to have: the fault is rare on boards a human drew, and the reading is common because most real boards route on a poured face
courtyard overlaps found by this round fixed — a courtyard pre-flight now runs before routing. Editing coordinates by pattern had let one part’s replacement land on another’s, which is how a divider ended up under an input resistor; every part on that board now carries its position explicitly

10. The reviewer’s pass, round five: no waivers

The instruction this round was two sentences long and changed the shape of the work: every item raised is critical and cannot be waived, and the evaluation is to be made from the output images. The second sentence explains why the first was needed. Three rounds of findings had been answered with argument rather than evidence, and the arguments were not being checked against the plots they were about.

The mechanism that was missing

pcb review --map findings.png draws the board from its own geometry and puts a numbered mark at every finding that carries a position, keyed to a legend. Rules opt in by putting coordinates in their details; nothing is parsed back out of a message.

It found its first thing immediately, and it was a lie of mine. The waiver on this repository’s FPGA board said its off-grid corners were the QFN’s escape fan and therefore unavoidable. The map showed the marks: not one of the hundred and forty-eight was near the QFN. They were on the power block, on the flash bus, on runs crossing open board. A count in a list can carry an excuse; the same count drawn where it happens cannot.

That is the case for the mechanism, and it is why it went in before anything else this round.

What went into the tool

built in kind what it catches
pcb review --map mechanism every located finding, drawn on the copper with a legend
route.width_step rule a run widening away from the pad the neck was for - the narrow part already set the current
route.under_package rule another net threaded under a package body: no plane under it, no way to probe it
layout.connector_not_at_edge rule a connector the cable has to cross the board to reach
silk.unlabeled_indicator rule an LED or switch whose silk names the schematic line, not the function
courtyard parsing parser what a part occupies, so a terminal block is measured by its body and not its pads
route.acute_angle, route.odd_angle, route.right_angle grading moved from info to blocking: grading them info was calibration against what human boards do, and this repository’s subject is what a generated board must do
angle rule: pad junctions fix two branches leaving one pad have an angle between them and it is not a bend in either - six false findings on the filter alone

What the tool then made us fix

fixed how measured
bends at angles nobody chose the router works on a grid and the pads do not, so the segment joining a path to a pad landed at any angle. Each is now a straight leg plus a 45 into the pad, skipped where the knee would not clear off-grid corners on the four two-layer boards: 91 to 0
square corners at run joins a route arrives as several Track objects and the chamfer only looked inside one, so the corner between two of them was never cut. Runs are joined first right angles: 12 to 3
copper drawn twice identical segments deduplicated - a doubled end reads to the angle rule as a run folding back on itself the zero-degree findings
width changed mid-run the bridge outputs leave at 0.4 mm and stay there; the escape carries the current whatever the rest is widened to, and the note says what 0.4 mm is worth width steps: motor 4, filter 1, both to 0
headers a cable could not reach the carrier’s two breakout headers moved to the top edge; the board lost twelve millimetres of empty field under them connectors off the edge: 2 to 0
a junction that was only a coincidence the buck’s feedback ended at a coordinate its output rail happened to pass through. It ends on the capacitor pad it senses at, which is what the note always claimed one dangling end

What is not fixed, stated rather than waived

Every waiver covering an item the reviewer raised has been deleted - route.acute_angle, route.odd_angle, route.return_path, track.thin_power and silk.over_pad across five projects. What those rules report now stands as failure, because that is what it is. The FPGA board is the honest limit: a 0.5 mm four-sided QFN escaping on two layers cannot hold the 45 grid, keep its plane whole, and stay off its own package’s underside at once, and the board’s own notes have said so since it was written. The fix is an inner layer, not an argument, and it is the next thing to build.

11. The reviewer’s pass, round six: the last of the waivers

Round five stated the policy — nothing the reviewer raised may be waived — and built the mechanism that made the policy checkable. This round is the work that policy demanded, and it went deeper than the findings it started from: three of the five boards were failing for reasons that had nothing to do with the rule that reported them.

What went into the tool

built in kind what it catches
readability.text_over_wire rule a symbol’s designator, value or rating printed across a net — a value with a wire drawn through it is a value nobody can read off the plot
property positions parser where a symbol field actually prints. The parser had been dropping the coordinate, so nothing downstream could ask
hide as a bare atom parser fix KiCad 7 writes (effects ... hide), not (hide yes). Reading only the second form made every hidden field visible, and forty hidden designators drowned the real findings
Design.keepouts mechanism rectangles of board closed to the router on both faces, so a layout can say which side a connector is approached from
per-net back_cost mechanism a signal’s plane crossing priced against the front-side detour that avoids it, per net, so ground is not charged for its own plane
route.width_step: the neck rule fix the rule’s own docstring said a width change is honest at a pad or at the edge of the pin field that forced the neck, and only the first half was implemented. A 0.5 mm row holds 0.2 mm and nothing wider, so every fine-pitch escape was a finding with no fix but an argument. The narrow side now gets the same power_neck_mm budget track.thin_power gives it

What the tool then made us fix

fixed how measured
fields printed across nets every field now states the spots it would accept, in order, and takes the first that prints over nothing — measured with the same rectangle the rule measures. The pin stubs and the PWR_FLAG’s own value were missing from the picture the placer looked at; they are in it now text_over_wire: buck 11, motor 6, filter 17, carrier 7 — all to 0
copper laid down and walked back along joining two routes at a shared end left the polyline going out past the join and straight back to it. The overshoot carries no current and comes out zero-degree corners: filter 1, motor 1, both to 0
corners cut off the pads they were reaching once two routes meeting at a pad are merged, the pad is an interior corner like any other and the chamfer cut it — moving copper off the pad and leaving the net unconnected, invisibly. Both clean-up passes now hold every pad and every track end still unconnected nets: buck 2, filter 3, carrier 1 — all to 0
plane islands that reached ground but not each other touching some ground copper was enough to keep a piece of pour. Two pieces each holding one bypass cap’s ground pad are still two pieces. The far side of the board is a node in the graph now, every via and through-hole pad an edge to it, and a piece survives only if it can be walked from there zone-island DRC pairs to 0
the motor board’s whole right-hand third four logic signals left the package on the east side, went over the top of the board, round the outside and back into the header from behind — 190 mm of copper for a 40 mm net, and four back-layer runs each cutting the plane under the track that fed it. The header now sits where the fan lands, pin for pin; the small caps sit in the middle band with the supply pins that own them; and VM crosses the plane on one stated link so that the signals cross its cut at right angles instead of going round return_path 4→0, detour 1→0, acute_angle 1→0, pour_fragmented 1→0, thin_power and width_step to 0
silk printed across pads the board id was written at the bottom centre whether or not the bottom centre was a module; the pin legend went toward the middle of the board, which on a carrier is the module it is labelling; a designator stayed where its library drew it, which on a part with pads on three sides is the middle of a pad. All three ask first, and the pin legend picks its side by the area it would take rather than by whether it collides silk.over_pad: carrier 8→0, motor 5 DRC silk warnings→1
the carrier’s plane in three pieces its two headers and the module run the length of the board and the pour could not get past their pin rows at either end. Six millimetres of board below the last pin is what a plane needs to be one plane pour_fragmented 2→0
rails escaping at signal width a supply pin leaving a fine-pitch row at 0.3 mm and widening two millimetres later is a step nobody chose; widening the far half only moves the complaint to the thin one. The row’s pitch sets the width, and the pin leaves at the width it keeps width_step and thin_power to 0 on the motor and filter boards

A note on which KiCad reads the file

The zone-island errors above are reported by KiCad 9’s DRC and not by KiCad 10’s, on the same file. The fill these examples write is a set of overlapping rectangles rather than one traced polygon, and the two releases disagree about when overlapping fill polygons are one piece of copper. The generator writes KiCad 9 format because that is the oldest release in the CI matrix; the verdicts quoted here are KiCad 10’s, which is the default the toolkit runs. Both are recorded rather than reconciled, because the disagreement is real and a reader meeting it deserves to know.

12. The reviewer’s pass, round seven: what the plot showed

Round six closed the findings the tools could see. The reviewer then sent thirteen screenshots with circles drawn on them, and the circles fell into two groups: red, strings printed through each other on the schematics, and blue, copper that leaves a pad, travels three sides of a rectangle and arrives a few millimetres away.

Neither group had a rule. readability.text_over_wire measured text against nets, and nothing measured text against text; route.detour weighed a whole net, and a net with six good connections and one bad one averages out under any ratio worth setting. So both got one.

What went into the tool

built in kind what it catches
readability.text_over_text rule two printed strings whose extents overlap, or a string printed across a symbol body. The netlist does not change, KiCad does not complain, and the only way to see it is to look at the plot - which is what this does arithmetically
route.wander rule one continuous run of copper - pad or junction at each end - longer than wander_ratio times the shortest way between those two ends that clears the packages in between. Where route.detour asks whether a net is long, this asks whether a track goes out and comes back
the wander baseline mechanism a run from one end of a package to the other cannot take the straight line, because the straight line is through the package. The baseline walks the perimeter of whatever the line crosses, so a feedback wrap is measured against going round rather than against going through
Label.angle / Label.justify parser which way a net label reads. Without them a label’s extent was a guess, and half of them were guessed backwards

What the tool then made us fix

fixed how measured
net labels printed through the part next door a label’s anchor cannot move - it joins the net by sitting on the wire - but the direction it reads in can. Each name is now offered the four quarters of its anchor and takes the first that prints over nothing. “Away from the pin” is still the first choice, but a five-character name on a 2.54 mm stub is twice as long as the stub, and away regularly meant straight into a diode text_over_text: buck 1, motor 2, carrier 1, fpga 7 - all to 0
four grounds printing GNGNGNGND a power symbol’s name had a fixed offset and no collision check at all. Four grounds hanging off one row of pins put four “GND”s on one line a pin pitch apart. The name is now offered the row below and either side of the stem the seven overlapping pairs on the FPGA sheet, to 0
PWR_FLAG printed across the module it declares the flag’s own name checked wires and nothing else, so on the carrier it landed on the forty-pin module. It now checks symbol bodies and the names already placed, and climbs a ladder outwards - on that sheet the nearest clear air is three text rows away, because the strip beside the module is its pin legend carrier 1 → 0
a plane crossing priced as a prohibition at forty times the front-side cost per millimetre, one millimetre of crossing buys a forty millimetre tour, and the boards had grown them. Thirty is where neither route.wander nor route.return_path fires: below it the router takes short back-layer hops that cut the plane under the same net’s own front copper opamp +5V 4.4x → gone; buck, motor and carrier to 0 wander findings
copper straightened off its own pads write_variant resolves the routes a second time, so the straightening pass ran again on the routed design - where the run into a pad and the run out of it have been merged into one polyline with the pad as an interior corner. Straightening through it took the copper off the pad. Pads are pinned now, as track ends already were opamp: 2 unconnected nets, to 0
stitching vias measured as circles the stitcher kept a via clear of a track by its radius; check_board measures the same via as a square, and the corner is 0.17 mm nearer. One board would not build opamp: 1 short, to 0

The corners the exemption was hiding

The reviewer then said the 135 degree bends and the odd-angle runs were still there. They were, and the rule could not see them: route.acute_angle skipped any corner within a pad’s radius, and a 0805’s radius is 0.47 mm, which covers the five or six ordinary corners a chamfered pad entry leaves inside it. Eleven of the thirteen hairpins on these five boards landed in that shadow, because a pad is exactly where a route that has to double back does it.

fixed how measured
the exemption itself it is the pad’s connection point now, not a disc around it. Two branches leaving one pad are still exempt - the pad’s own copper fills the wedge between them, so it is not an acid trap - but nought degrees never is: that is one run drawn twice and no pad excuses it 13 hairpins, of which the rule had been reporting 2
one run drawn on top of another the shorter is inside the longer and carries nothing it does not. Both get trimmed: dropping only the duplicate leaves the other hanging over the gap, which is route.stub. It runs before the runs are joined, while the pair is still two tracks sharing an end 180 degree reversals: fpga 2, filter 3, carrier 1 - all to 0, and the motor board’s return_path finding went with the 11 mm of copper it was measuring
straightening onto no grid at all the round-trip remover from the last round would replace a stated route with its direct line whenever that was clear - and between two pads at whatever coordinates their packages give them, the direct line is usually at no angle anyone drew. It made a nine millimetre run at 169.7 degrees on the FPGA board off-grid segments: 1 → 0, and 0 on all five

Three corners survive, all on the FPGA board, all where a knee lands a fraction past the point its own two legs cross: legs of 0.07 and 0.25 mm on two of them, 0.15 and 0.5 mm on the third. Declining to take a knee that short is worse - the segment then keeps the angle it had, and measuring that put 157 off-grid segments back across the five boards - so the nub stays and the rule reports it.

What is still there

The FPGA board is not fixed. It carries route.detour (VCCPLL at 5x), route.return_path on seven signals and route.wander on four runs, and one DRC unconnected item. Every one of them is a floorplan question on a 48-pin QFN with two layers, and each attempt at it costs the better part of an hour of routing; they are recorded here rather than waived. The opamp board keeps one wander finding: every placement tried moves it between +5V and OUT without removing it, which is what a congested two-layer board looks like when the measurement is honest.

13. The reviewer’s pass, round eight: the boxes were the wrong boxes

Round seven took readability.text_over_text to zero on all five sheets and called the red circles closed. Re-rendering the plots said otherwise: an LED with its ratings printed across its own arrows, a note running through a capacitor’s designator, “50ppm” and “GND” printed on the same line, a sentence lapping the title block.

The rule reported none of them, and for one reason each time - it was measuring a rectangle that is not the one on the paper.

What went into the tool

built in kind what it catches
Symbol.outline / Symbol.body_bbox parser the shape KiCad actually draws. A schematic embeds the full library definition of every symbol it uses, graphics included, so the outline is knowable from the file alone. It had been guessed from the pins, and an LED’s two pins span 2.54 mm while its emission arrows reach 4.6 mm the other way - which is why a value “cleared of the pins” printed through the part
Symbol.property_angle / sch_review._field_box parser + rule which way a field reads. KiCad adds the symbol’s own rotation to the field’s, and where the sum is half a turn it keeps the glyphs upright by swapping the justification instead. Every rotated part’s fields were therefore measured on the opposite side from the one they print on - the check and the plot were looking at different rectangles
notes in readability.text_over_text rule a design note printed through a designator, a value or another note. The rule had compared fields with fields and fields with symbols, and left the third kind of string on the sheet out of it
readability.margin_intrusion measured by extent rule a note that ends on the title block. It had tested the anchor point alone, and the sentence that ran into the carrier’s title block started 12 mm clear of it and was 67 mm long. The block’s own geometry was wrong too - 110 mm wide inside a 10 mm frame and 44 mm tall on a sheet that fills its comment rows, not the 115 by 30 the rule assumed. On KiCad’s demo corpus this takes it from 1 sheet and 5 findings to 2 and 7
a 1.9 mm text row rule two strings one pin pitch apart. The row had been measured at 1.6 mm, which reads 1.27 mm of separation as a third of a millimetre of overlap - inside the tolerance, and on the plot one unreadable word. KiCad stacks a part’s own fields on a 2.54 mm pitch, and 1.9 mm is what separates those two cases

That last change is why the corpus numbers moved: text_over_text now fires on 15 of KiCad’s 18 demo sheets rather than 14, and text_over_wire on 11 rather than 10 - but text_over_wire reports 602 findings where it reported 942, because the boxes that were on the wrong side of a rotated part have stopped being counted. Both stay info, promoted to errors by the ai-generated policy.

What the tool then made us fix

fixed how measured
ratings printed through their own part the generator measured the block against the pin column plus 2 mm. It reads the same library outline the rule reads now, and a part’s own body is on the list its designator and value have to miss buck’s D2, the carrier’s D1 and D3, the motor board’s D2 and C3, the opamp’s R1/R2/C3/C6
a lying part’s ratings placed blind only the upright branch searched for clear paper; a part on its side stacked its ratings 5.08 mm under its body whatever was there. Under the FPGA board’s oscillator sits its own ground symbol, so “50ppm” printed through the word GND. The flat case now asks the same question - which row, how far left or right fpga: X1 Tolerance over #PWR20 Value, to 0
every rotated part’s fields on the wrong side the generator picks a side and then writes the justification KiCad will render, flipping it where the symbol’s rotation flips it. Placing text against a rule while writing a file the rule reads differently is not placement at all all five sheets
a note through the circuit, and through the title block notes are emitted last and are the string with room to move: a field is anchored to its part and a label to its wire, but a sentence only has to be near its subject. Each block now slides to the nearest clear paper within a centimetre, measured against the symbols, every string already placed, the wires, and the title block motor: the bridge note off D2; fpga: the bank note off C12; carrier: the plane note out of the title block
a note ending on the title block the carrier’s plane note is fifty-eight characters starting in the right-hand column: 67 mm of sentence with 55 mm of paper beside it. No amount of sliding fixes a line wider than the space left for it, so the anchor moved to the left column - which is the case the slide cannot handle, and worth saying so carrier: margin_intrusion 2 → 0
a designator with a wire either side six candidate spots above a two-pin part are none at all when both flanks carry a net. The ladder reaches outwards now, and a PWR_FLAG’s name ladder was half duplicates - it read justify left for the spot to its left, which puts the same box back over the flag text_over_wire on the reviewed sheets: 8 → 0

The fixture moved too. tests/fixtures/example_project had 10k printed down its own resistor and 100n on both capacitors’ plates - three collisions that had been there since the fixture was written and that nothing could see.

Where the five sheets stand

readability.text_over_text, text_over_wire and text_over_symbol all report 0 on every reviewed/ sheet, measured against the drawn outlines rather than the pin boxes. The one collision left anywhere in the set is on opamp-filter/as-generated, where two net labels print through each other - which is the variant whose job is to be wrong.

14. The reviewer’s pass, round nine: the copper the plot showed

Round eight closed the schematic side. The reviewer then said the artwork was not fixed either, and re-rendering the boards agreed: three defects were plainly visible and no rule reported any of them.

What went into the tool

built in kind what it catches
rip-up and reroute for tidiness mechanism _route_all now reports what each track cost against route.wander’s own baseline - imported rather than copied, because two implementations of one measurement drift - and the worst tour goes to the front of the order and the set is routed again. A track that still tours from first pick has nowhere better to be and is left alone
shortest first mechanism the default routing order. A thirteen millimetre connection has few ways to be made and a forty millimetre one has many, so the short ones should choose while there is still room. It is also what a fresh clone starts from, having no learned order
layout.zone_outside_outline rule a zone drawn wholly off the board. KiCad stores a footprint’s zones in board coordinates while everything else in a footprint is relative to it, so a placer that moves the pads and forgets the zone leaves the keep-out where the library drew it
silk.text_over_text rule two silkscreen strings on the same side printed through each other. The schematic has had readability.text_over_text for two rounds and the board had nothing, though the board is the harder case: a sheet can be zoomed and a bare board cannot

What the tool then made us fix

fixed how measured
a track that tours the board to cover thirteen millimetres the op-amp’s feedback wrap goes from one side of a SOT-23-5 to the other. Routed last it took fifty-six millimetres, because everything nearer was already spoken for. Routed first it costs nothing opamp /OUT 56.5 mm at 4.3x → gone; the board goes from FAIL to PASS
a keep-out at the origin the Pico module’s two pad keep-outs had been at (0, -6) for four rounds - off the board, keeping nothing out. DRC is silent because an empty region violates no rule, and the only visible sign was the plot: “fit to page” fits the bounding box, so every view of that board came out at half scale in one corner with the rest blank carrier: 2 → 0, and the board is legible in the documentation for the first time
silkscreen printed through silkscreen the connector legends are placed before any designator and the designators get out of their way - a legend names one pin of one connector and has to sit against it, while a designator can go anywhere legible. Same order the schematic side uses for a label and a field. A pad is weighed fifty times a courtyard, so reaching further out along a row never buys ink on copper silk.text_over_text 3 → 0 and silk.over_pad 0 across the five; the fixture had “IN GND OUT” printed through “J1”
the FPGA board eleven rip-ups and three re-orderings later: route.detour on VCCPLL at 5x gone, route.wander 4 → 2, acute corners 3 → 2, route.return_path 7 → 6, and the last DRC error - one unconnected item - gone. Board findings 1/10/8 → 0/9/8 5 blocking → 3

What re-ordering cannot do, and what happens when it tries

Promoting a wandering net to the front takes a lane some other net had. On the FPGA board the sixth promotion left I2S_SCK with nowhere to go, and the first version of the loop called that an impossible floorplan and refused to write the design at all - a generator that had worked now exited with an error.

Feasibility is the hard constraint and tidiness is not. The loop keeps the last order that routed everything; when a promotion makes a net unroutable it goes back to that order, stops chasing tours, and writes the board with the tours still in it. Routing is deterministic in the order, so the restored pass is the one that already succeeded and the loop terminates. The learned order file records rip-ups only: a rip-up is knowledge - that net has to go early or it has no room - while a promotion for tidiness is a guess, and writing those down poisoned the file for the next run.

What is still there

Six nets on the FPGA board run over cuts in the back-side plane, two runs still wander and two corners are still acute. The return_path six are the two-layer choice itself: a 48-pin QFN with no inner layer has to bring the SPI bus out somewhere, and re-ordering cannot make a plane that is not there. They are recorded here rather than waived.

15. The reviewer’s pass, round ten: learn from the boards people drew

The reviewer’s brief for this round was direct: relying on the autorouter and the numbers alone is not enough - go and organise what pictorial correctness looks like from ordinary circuits and open designs, and improve the drawing itself. So this round started not in the generator but in KiCad’s own demo corpus: sixteen parsable human-drawn boards, measured and looked at.

What the corpus says

tools/board_signature.py (new) reduces “looks autorouted” to five numbers per board. Hand-routed two-layer work clusters tightly: 10-47% of copper on the second face, median segments of 1.8-3.5 mm, 9-25 corners per decimetre, 91-98% of them 45s. Rendering interf_u beside our boards made the same point visually: a layer has a direction, a bus travels as a bundle in one corridor, and a person covers an offset with two strokes - the long straight and one diagonal. Against that baseline the generated boards read as machine work for exactly three reasons: everything on one face, the router’s 0.25 mm cell as the drawing’s rhythm (op-amp median segment 0.75 mm, 38 corners/dm), and a uniform via carpet.

What went into the tool

built in kind what it does
tools/board_signature.py tool the five numbers, runnable over any mix of demo and generated boards, so the comparison is repeatable rather than an impression
Router.route(follow=...) mechanism cells beside a sibling net’s path are discounted, so a bus (nets sharing a name prefix - I2S_*, SPI_*) travels as a bundle: the parallel-lanes look wins every tie, and the discount is a fraction of a step so it never buys a detour
_doglegged pass redraws every wiggly stretch as the human stroke - the straight along the dominant direction plus one 45 - when the dogleg is clear, splitting the stretch in half recursively where a crossing blocks the whole. Endpoints never move
the copper oracle’s pinned() fix reshaping passes may not move copper off any point inside a pad of its own net - the centre alone was not enough, because a routed run can touch its pad off-centre and the overlap is the connection. Found by the pass pulling C4’s connection off by a hair: one DRC unconnected item, invisible in the shape
seam guard fix a dogleg meeting the copper it did not redraw can turn back on itself, and the chamfer pass carves that seam into a 45-degree acute corner. Junction turns are limited to a right angle, and a split is taken only when both halves redraw
purposeful stitching mechanism the 6 mm via carpet is gone. A hand-stitched board puts vias where the plane needs them - a ring at the rim, and beside every place a signal crosses on the back layer, because that is where the plane is cut - and only then a coarse 12 mm mesh so no orphaned pour floats. Vias per decimetre: buck 41 → 18, carrier 28 → 13, all five inside the corpus band, and layout.pour_fragmented still reports nothing
the oracle updates as it goes fix the clearance oracle was a snapshot, and a pass that redraws two tracks against a snapshot lets each move into the corridor the other just left - I2S_DIN and I2S_LRCK both doglegged into one lane and ended 0.03 mm apart. Every accepted redraw now goes straight back into the oracle
route_keepout=("U4", "U2") design no foreign copper under the boot flash or the DAC. Fencing U4 alone just moved the 1.2 V rail under U2, which is the worse place - the DAC is the one analogue part on the board. route.under_package is the rule that kept saying so
_net_of in the pcb parser fix KiCad also writes the name-only (net "VCC") form - the pic_programmer demo does - and reading the name as a code crashed the whole board. The corpus grew from 15 parsable boards to 16

Measured against the corpus

board med. segment corners/dm vias/dm verdict
motor-driver 1.77 → 1.95 mm 18.5 → 17.5 13 → 8.5 in the human band
opamp-filter 0.75 → 0.88 mm 38.0 → 33.5 13 → 10.5 direction right; the densest board in the set affords the least redrawing once every dogleg must clear DRC with a full cell of margin
buck-5v 5.75 mm 6.2 41 → 18 already past the human band - six parts in a row
pico-carrier 6.31 mm 7.3 28 → 13 likewise
fpga-audio 1.43 → 1.54 mm 24.8 → 17.8 13 → 7.8 in the human band on every style measure; the floorplan debt below is a different axis

All four still pass their gates with KiCad’s own DRC, zero blocking. The first version of the pass scored better - op-amp at 1.50 mm and 23.5/dm - and was wrong twice: it pulled a connection off a pad (the pinned() fix) and it parked a redrawn segment at exactly the 0.2 mm clearance limit, which DRC fails by the width of a rounding error. The honest numbers are the ones above, and the two failures are why the oracle now demands a full router cell of margin.

The FPGA board: two fences and an honest trade

route.under_package kept reporting the 1.2 V core rail under first the boot flash and then the DAC - the two bellies a rail sneaks through when the escape field has taken everything else. Fencing both (route_keepout) is the right call and the board pays for it: the rail now hauls 131 mm to cover 40 mm, route.detour is back on +3V3 at 4.3x, and the gate holds four blocking findings (2 acute corners at the pour edge, the detour, 4 return_path nets, 4 wander runs). What this board needs next is not more re-ordering - the rip-up loop rolled back twice trying - but a stated 1.2 V spine: the designer’s power backbone, written into the file with its own waypoints the way the motor board states its VM link. That is the next round’s work, and it is named here rather than waived.

What this round is really about

The seven rounds before this one built rules: each finding became a number and the generator chased the number. This round built a reference: the drawing habits of people, measured from their boards, stated in the authoring guide (“The numbers behind the look”), and pushed into the generator as habits rather than penalties - travel with your bus, draw long strokes, keep a direction per layer. The difference shows in what happened to the op-amp board: its number barely moved, and the plot still got calmer, because the strokes that did redraw are the long ones the eye follows first.

16. The reviewer’s pass, round eleven: “are the other checks really passing?”

The reviewer looked at the plots, saw tracks driven straight through each other, and asked the only fair question: with crossings that blatant, are the other checks actually passing?

They were, and both halves of that are worth writing down. Measured directly - every pair of same-layer segments on every reviewed board - there is not one crossing between different nets: KiCad’s DRC reports zero errors, zero shorts, zero unconnected items on all five, and the DRC is not being fooled. What the plots show is thirteen places (FPGA), one (op-amp) and one (buck) where a net crosses itself. Same potential, so DRC has nothing to say; no length ratio catches it, because the loop can be short; and no rule of ours looked. The checks were honest. The check list had a hole exactly the shape of what the eye catches first.

What went into the tool

built in kind what it does
route.self_crossing rule a net whose own copper crosses itself on one layer, warning, promoted by ai-generated. On KiCad’s demo corpus: 6 of 16 boards carry one to three, at dense escapes - ours carried thirteen on one board
_unlooped pass the fix, as a graph question: split every same-net crossing so the X is a node, and while any cycle remains in the net’s copper, remove the longest junction-to-junction chain of the cycle. Connectivity is kept by definition - a cycle has two ways round - and the amputated X is left as an ordinary corner. The pour net keeps its mesh
the oracle refuses new X’s guard _doglegged and _straighten may touch their own net’s copper - that is a junction - but not cross it, or they would redraw the loops the cutter just removed

What the cutter got wrong twice before it was right

Both failures were the same lesson: KiCad’s connectivity is geometric, a track graph is endpoint topology, and the difference is exactly a pad. The escape fan draws a deliberate micro-hook inside an off-grid pad - copper overlapping copper is the connection - and the graph saw a dangling loop feeding nothing, called it redundant, and amputated a pad’s only feed: one DRC unconnected item per pad, invisible in the shape. The cutter now treats any node inside a pad of the net’s own as an anchor, refuses to touch a cycle that sits wholly inside one pad, and splits segments that pass over a pad so the feed is a node the cut has to respect. The DRC run that verifies all this is the point of the reviewer’s question.

Where it landed

Self-crossings on the five reviewed boards: 0, 0, 0, 0, 0 (were 13, 1, 1, 0, 0). Cutting the loops removed real copper, and the FPGA board got lighter for it: route.detour on +3V3 retired outright, route.wander 4 → 3, route.return_path down to 3 nets, and KiCad’s DRC is clean again - zero errors, zero unconnected. Its gate is down to three blocking findings, all floorplan, all named in round ten’s “what this board needs next”.

17. The reviewer’s pass, round twelve: the circle on C15

The reviewer circled one spot on the FPGA board’s front render — a cluster of 45-degree tracks converging on one 0402 — and asked whether the judging is buggy, or missing a fundamental viewpoint.

Measured, the spot was electrically blameless: every track in the circle is the same net (the 1.2 V rail), nothing crosses, DRC is clean, and no rule had anything to say. The first suspicion — that a pad being used as a junction is itself the defect — did not survive contact with the corpus: counting pads that carry three or more track arms across the 16 parsable KiCad demo boards finds them everywhere people route by hand (10 on the Jetson carrier, 95 on the VME board). Humans tee on pads freely. That is not the missing viewpoint.

The missing viewpoint was one level down, in the router’s contract: a link was only allowed to finish on the pad it names. A net with three branches therefore funnelled all three into one capacitor pad — the junction could not form anywhere else, however awkward the convergence, because nowhere else was a legal place to stop. People do not route under that constraint: they tap the trunk at the nearest point, and the junction lands where the geometry is shortest.

What went into the tool

built in kind what it does
tee in Router.route router the search may finish on any cell whose centre lies exactly on the net’s own already-laid copper, not only on the named pad; the junction then forms wherever is nearest
_tee_component guard only copper already electrically joined to the link’s endpoint counts — through shared points, the net’s vias and the net’s pads — or the named pad is left unconnected and only DRC would notice
pad-named endpoints only guard a link aimed at a bare coordinate is aimed at the stated end of a trunk someone drew, and stopping short of it strands the trunk’s tail as a stub; a pad is a terminal in its own right, so copper between a tee landing and a pad still ends somewhere real
_absorb_tee pass a landing mid-segment is inserted into the trunk’s points as a stated corner, so every reshaping pass pins the join the way it pins any other track end

Each guard cost one broken board to learn: the first regeneration left a 0.5 mm stub of the op-amp’s 5 V trunk hanging in air, because a link had teed onto the trunk half a millimetre before the stated coordinate the trunk was drawn to end on.

The stated 1.2 V spine

Round ten named the FPGA board’s next work: state the 1.2 V rail the way the motor board states its VM link. The tee is what makes a stated spine worth having — branches can tap it anywhere — and the corridor came from looking at what the router had been doing instead: its consumers sit on both sides of the FPGA, every east-west lane south of the package is a comb of SPI escapes, and the link-by-link answer was a 122 mm tour of the board’s south edge to cover 39 mm. The one corridor nothing else can use is under the FPGA’s own die: the QFN’s pads are surface copper, the strip between its south pad row and its ground-via grid is empty on the back, and the rail is the package’s own supply, so nothing runs under a part it does not feed. One straight stroke on the back, a via at each end, and the links tap it.

Where it landed

The circled defect is gone as a class, not as an instance: on the regenerated FPGA board C15 is fed by one arm, the junctions sit on the trunks, and no small pad on any of the five boards carries more than three arms - which is where the human corpus sits too. The tee also made the boards lighter: the op-amp board lost 12% of its copper, the FPGA board 15% (1860 mm down to 1583 mm), because a branch that may stop at the nearest trunk no longer duplicates the trunk’s own distance. Crossings stay at zero on all five, KiCad’s DRC stays at zero errors, and the four boards that passed their gates still pass them with the same waivers.

The FPGA board also finally got the spine, and the round shows why the order of those two things mattered: regenerated with the tee alone, the rail still toured (122 mm for 39) and the signals it displaced put seven nets over plane cuts - worse than the three the board started with. The spine reclaimed the corridors: route.return_path is back to three nets (all SPI, 11-28 mm), route.wander no longer names a rail at all (two signal detours remain, 2.1x and 2.6x), and route.acute_angle holds two 45-degree corners at the pour edge plus one folded corner the clean-up passes have not learned to unfold. Three blocking findings, all floorplan-class, all still stated by the gate rather than waived - the honest price of a QFN-48 on two layers, now without the tours that used to sit on top of it.

Stating the spine took three broken boards of its own, each caught by check_board before anything was written: a through via parked in the west escape comb lands on whichever escape line owns that lane; at y = 42.5 the east via missed the QFN’s south pads by a tenth of a millimetre; and the die centre belongs to the exposed pad, so the east end may not drill at all - the stroke and its tap meet on the back instead, with no layer change. The corrected stroke sits at y = 42.25: a quarter-millimetre off the pads’ inner ends, and still on the router’s quarter-millimetre grid - off it, no tee could land on the stroke, which would have defeated the reason it exists.

18. The reviewer’s pass, round thirteen: the plane is a drawing too

Six circles this time, five of them about the same thing seen from different corners: the ground plane is part of the drawing, and the generator treated it as leftovers. A wedge of pour tapering to a point in a 45-degree corner on the motor board; the op-amp board’s 5 V feed running outside its own shell, the outermost copper on the board with no return beside it; the carrier’s top edge a blank strip and its back plane sliced into panels; the FPGA board’s bottom band and line-out pocket fenced off by the SPI bundle; and - the sixth - the FPGA sheet’s power-entry block printing over the frame’s ruler strip.

What went into the tool

built in kind what it does
_pruned_tongues fill pass a dead-end strip of pour narrower than ZONE_TONGUE (0.9 mm) retracts until the plane is wide again - a narrow channel touches neighbours on two sides and stays; a strip feeding this net’s own pad or via stays too. The acid-trap wedges in bent corners go away as a class
pour to 1.2 mm of the edge design the pour rectangles stop 1.2 mm inside the outline instead of 2-3 mm, so an edge-hugging trace keeps shell copper outside it and the board’s outermost feature is ground again
per-piece stitching _stitch_vias every piece of the front pour over 8 mm² holds a via of its own, placed against the same clearance checks as the mesh - and the pieces are taken before the orphan drop, so a strip nothing else reached gets a via instead of staying a blank
ZONE_CLEARANCE 0.4 → 0.25 fill at 0.4 the web between a 2.54 mm header’s pads came to 0.34 mm - under ZONE_SLIVER, so every column became a full-height slot. At 0.25 the web is 0.44 mm and the plane flows between the pins. DRC clearance on these boards is 0.2, so nothing legal got closer than allowed
tee stays 2 mm off the pad router the round-twelve tee moved junctions off the pads - and promptly fed the carrier’s bulk capacitor through a one-millimetre stub from the rail. Within two millimetres of the goal a tee saves nothing and costs the flow-through, so there the pad wins
readability.margin_intrusion reads fields rule the rule measured pins and notes but not symbol fields, and power symbols not at all - and the FPGA sheet’s PWR_FLAG printed its name across the ruler strip. Fields are text like any other now, power symbols included. Seven findings across KiCad’s 18 demo projects - people do park a label on the frame here and there, which is why the rule stays info and only the ai-generated policy promotes it - and three real catches on our own sheets the moment the rule could see: the motor board’s power connector, and two more corners of the FPGA sheet

What stayed by hand

Two of the circles were placement, not machinery. The carrier’s 22 µF bulk capacitor sat a millimetre south of the VSYS run, so the rail passed straight by and fed it through a stub - it now sits on the run, current in one pad-side and out the other, no stub at all. And two power-entry connectors (the FPGA board’s and, once the extended rule could see, the motor board’s too) moved eight millimetres in from the sheet edge so their printed names clear the frame.

One cut stays, stated rather than hidden: the carrier module’s own pad columns still slot the back plane top to bottom. The module footprint carries a 2.2 mm unplated pad behind every castellation at 2.54 mm pitch, which leaves negative room for a web at any legal clearance - that is what soldering a forty-pin module onto two layers costs. The panels it makes are tied along the full top and bottom bands the wider pour now reaches, and the middle panel carries the module’s eight ground pins straight into the plane.

Two more circles while the paint dried

The reviewer’s next pass found the hook and the step (round thirteen’s last two circles, on the op-amp board): two runs of the reference rail down the same lane - the second rode the first, because a net’s own copper costs the search nothing and nothing forbade travelling along it - and a width that changed from 0.5 to 0.3 mid-run, where power-width links met a 0.3 mm escape. The ride is now priced (crossing own copper stays cheap, travelling along it never wins), VREF runs 0.3 end to end with a waiver stating its current, and the divider’s 5 V feed keeps the trunk’s width to the junction. Measured after: zero doubled runs and zero mid-run width steps on all five boards.

Where it landed

All four fast boards pass their gates. The FPGA board still does not, and its three blocking findings have a single name now: route.return_path holds four SPI nets at 11-20 mm over plane cuts (the worst used to be 27), route.acute_angle two corners, and route.wander three detours all on the 3.3 V rail - the twenty-link net that wants the same stated spine the 1.2 V rail got. That is the named next work. Everything else measured clean across all five: zero crossings, zero rides, zero mid-run width steps, every pour piece over 8 mm² holding its own via, and the planes’ outer shell reaching to 1.2 mm of every edge.

19. The reviewer’s pass, round fourteen: the fold, correctly this time

The reviewer accepted the round and re-reviewed - with one correction. Round thirteen read the two circles on the motor board as pour wedges and pruned the pour; the circles were about the bends. Each circled corner is a legal 45 on its own; the pair of them - a 90 and a 45 with a tenth of a millimetre between - turns the run 135 degrees from its direction of travel, folded into half a millimetre. The angle rule is structurally blind to it: it measures one corner at a time, and every corner passes.

What went into the tool

built in kind what it does
route.hairpin rule two same-direction corners within 1.2 mm of track whose signed turns sum past 100 degrees. Signed, so a staircase’s alternating 45s cancel; arms shorter than 0.8 mm are a clearance artefact skirting a via, not a legible fold; a fold whose middle sits inside its own pad is the escape fan’s micro-hook and stays. Six findings across KiCad’s 18 demo projects - info, promoted only under ai-generated
_spread_hairpins pass the generator’s answer: the fold’s middle leg stretches to 1.2 mm and the exit re-doglegs onto the far end of the outgoing straight. Aimed at the first vertex it folded straight back - the exit carries a redundant collinear point half a millimetre out, and the first attempt found it the hard way
stitch vias stay inside the pour guard a candidate offset from a track end can land past the pour and onto the outline itself - two did, one dead on the board edge - where it is an edge violation and an orphan at once. check_board cannot see either; KiCad’s DRC caught both, and the candidate filter now owns the bound

The motor board’s two circled folds are now wraps: the same 135 degrees, drawn as spread 45s over two millimetres, which is how a person turns back. All five boards measure zero hairpins, zero crossings, zero doubled runs, zero mid-run width steps.

Where the FPGA board stands

Twice this round the FPGA board was re-routed from a cold cache, and the two draws tell the story of what the rip-up loop’s learned order was worth: the first came back with one bad net (the line-out pair touring at 4.3x, route.detour and route.wander both naming it) on top of the standing SPI-over-cuts debt; the second came back worse - four tours, a stub and an off-grid corner - and was discarded for the first. The committed board is that better draw: zero DRC errors, zero hairpins, zero crossings, and four blocking findings - three corners, one line-out tour, five SPI/CRESET nets at 11-19 mm over plane cuts, and the tour again as route.wander. The 3.3 V spine and a floorplan that gives the line-out pair a corridor stay the named next work.

The reversal of fortune is worth writing down: the environment reclaimed the container twice during this round, and each reclaim rolled the working tree - and the route cache with it - back a day. The code came back from the remote in minutes each time; the FPGA board’s route did not, because a route is an afternoon of rip-up learning keyed to code that no longer hashes the same. The five commits of history survived because every one of them was pushed the moment it existed. The lesson is already this repository’s working agreement; the round is what enforcing it looks like.

20. The reviewer’s pass, round fifteen: the arc, and the retraction

Two more corrections, both accepted in full.

The reversal is now a continuation of the escape, not a spread fold - and it took three passes to hear the drawing correctly. The first arc kept a lead stub out the old heading and took every 45 from there; the stub meant the trace stepped back to nine o’clock after the escape’s own diagonal had already left that heading, and the reviewer had to point at the bend twice. The final form has no stub: _spread_hairpins starts the turn right at the head of the incoming straight - where the escape’s 45 already points the line - stands it up square, takes the remaining 45s, and solves the equal leg length so the turn lands exactly on the outgoing line; when the exit leg is too short to land on, it carries the turn through the exit’s own 45 and lands on the straight beyond. On the motor board each of the two nets now draws one continuous curve from pad to bus - nine o’clock, half-past ten, twelve, half-past one, three - with the fold, its corners, and the three-millimetre ride to the fan column all gone.

And the round-thirteen tongue pruning is reverted outright. It was built on the misreading round fourteen corrected - the pour filling a bent corner was never the complaint - and the reviewer called the leftover retractions what they were: an incorrect fix left in place. The fill goes back to what the sweep makes of the copper, the guide sentence that recommended the pruning is gone, and the boards regenerate identically minus the missing wedges.

Where it landed: all four fast boards pass their gates; every board measures zero hairpins, zero crossings, zero doubled runs. The FPGA board’s fresh route reproduced its round-fourteen debt to the digit - three corners, the line-out tour at 4.3x, four nets at 11-18 mm over plane cuts - which after four independent cold draws looks less like luck and more like the floorplan fact the gate says it is. The 3.3 V spine and a line-out corridor stay the named next work.

21. The reviewer’s pass, round sixteen: the hole in the land

“パッドオンビアが多用されています。これは製造上の問題があるので避けてください” — via-in-pad is used a lot here; it is a manufacturing problem, please avoid it.

Six circles on the op-amp board, and the reviewer said plainly that the board in the picture was only where they had noticed it. They were right on both counts: eighteen vias across three of the five boards sat inside a surface-mount land.

A hole in a land is a hole solder wicks down during reflow. The joint above it starves, and nothing on the assembled board distinguishes that from a cold joint - it is the failure that ships. Via-in-pad is a real technique and a process: the barrel is filled with resin and plated flat before the board ever sees paste. A layout that has not specified that process may not draw it, and here nothing had.

The router was the source, and the mechanism is worth writing down because it is the same shape as the tee bug two rounds ago. _blocked skips obstacles belonging to the route’s own net - it has to, or a track could never reach its own pad - and the via placement test was built on top of it. So a layer change was legal on the very land the route started from, and a ground stub asked to reach the plane spent its via without leaving the capacitor. Pads are now marked as pads in the obstacle list, and a pad is a cell where a via may not go whoever owns it. The escape leaves the land first and turns its via beside it, which is the layout a hand would have drawn anyway.

The stitching pass had a smaller version of the same blind spot: it kept a signal land a via-radius plus clearance away and a ground land only a radius, on the reasoning that ground copper touching ground copper harms nothing. Electrically true, and beside the point - the solder does not know whose net it is wicking down. Both distances are the same now.

via.in_pad measures what is left: the copper gap from every via to every land, its own net’s included. The one exemption is the exposed thermal pad under a package, where the via array is what the datasheet asks for; nothing a single signal reaches is four square millimetres, so the two are told apart by size. The rule blocks under the ai-generated policy, because a generated board has no reason to draw a via in a land.

The test fixture had one too - twenty-five microns of overlap into C1’s ground land, invisible on any plot - which is the useful part of a rule that measures rather than looks.

What the ban cost, and what it bought

Forbidding the layer change on a land is a small rule with a large consequence: the FPGA board stopped routing. Five runs, five different nets, each one out of lanes in the corridor between the FPGA’s east escape fan and the codec’s west one - and every one of those nets routed on its own when asked, which is congestion looking for an order, not a floorplan with no lane.

Three things fixed it, in the order a layout would do them. The via spacing was guesswork and is now the fab’s: hole to hole goes from 1.2 mm to 0.9, half a millimetre of laminate between barrels at the shipped via, which in a 1 mm-pitch escape comb is the difference between a lane and no lane. Every decoupling capacitor’s ground via is now placed against its own pad rather than nine of them asking the search for one - and where it goes is chosen, not fixed: anchor_site takes the nearest position that clears what is already there, fanning either side of the direction facing off the part. The first cut used a fixed 1.2 mm offset and walled off the corridor its neighbour’s supply needed; the via was legal, and it was in the way.

Then the floorplan. Seven parts - four decoupling capacitors, the two 1.2 V ones and the LED resistor - were sitting in the eleven millimetres between a twelve-lane escape and a ten-lane one. They move four millimetres south into the empty band below, and the corridor carries the bundle it was always for.

That last move paid a debt three rounds old. The line-out tour - 88 mm of copper for a 20 mm net, route.detour and route.wander both naming it, reproduced across four independent cold draws and written down each time as floorplan rather than routing - is gone. Both rules report nothing. The FPGA board’s gate goes from four blocking findings to two, and what is left is the two-layer bill it has always been: three SPI nets crossing cuts in the plane, and three corners.

The lesson is the one the guides keep circling. A rule that forbids something the router was quietly relying on does not just cost that thing; it exposes what was propping the rest up.

22. The reviewer’s pass, round seventeen: making it, not just routing it

“ベタのクリアランスは考慮足りてないように思います。R/Cのあいだにまではいってしまっていて、クラアランス無視しているか狭すぎる。こらはDRCでも検出できると思っていましたが、未実施でしょうか” — the pour’s clearance looks wrong; it reaches in between an 0603’s pads. I thought DRC would catch that. Was it not run?

It was run. It was green. Both of those were true, and so was the reviewer, because the check and the drawing were looking at different polygons.

The fill in the file was ours - a sweep of the pour outline with everything of another net subtracted - written years of rounds ago because a comment here said KiCad’s own filler needs a display the container has not got. That was never checked again. pcbnew’s ZONE_FILLER runs headless in both images and fills a board in a second. Meanwhile pcb drc was passing --refill-zones, so KiCad replaced our polygons with its own before checking and reported on a board nobody had. Turn the refill off and the shipped fill has 199 pieces of copper KiCad calls isolated and reaches 0.075 mm from a foreign pad where the rule says 0.25.

So the zone is declared and left empty, KiCad fills it, and drc() stops refilling a board whose zones are already filled - that fill is what goes to the fab and what the plots draw. Everything the sweep had been approximating is now the board’s own rules: the clearance, the minimum web width, and the thermal relief the zone had been asking for all along and our filler had been ignoring.

That last one was the reviewer’s next point, and it arrived for free: every through-hole ground pad now sits in a gap bridged by four spokes, so an iron can bring the joint up without heating a hundred square millimetres of plane. layout.solid_pad_connection reports a board that turns it off.

The rest of the round is the same kind of work - the part of a layout that has nothing to do with whether the circuit is right:

Two bugs surfaced by placing parts at the edge, both older than this round: a designator was kept off the pads but not inside the board, so every edge part had its reference clipped; and _courtyard_box read lines and rectangles only, while a mounting hole draws its courtyard as a single circle - so the holes measured as taking up no room, and the first three fiducials landed on top of three of them.

23. The reviewer’s pass, round eighteen: room for the screw, ink on the board

Two things, from one screenshot of the opamp board’s top-left corner with two circles on it. One round the designator H1, printed above the board edge. One round the screw hole itself, sitting against the screw terminal’s body.

The hole was placed against the drawing, not against the screw

A MountingHole_3.2mm_M3 draws its courtyard as the drill plus a whisker: 1.8 mm of radius. What goes through it is an M3 pan head on a DIN 125 washer, seven millimetres of steel lying flat on the board, turned by a driver that wants more again. The placer avoided courtyards, so it put screw heads on capacitors and, on four of the five boards, inside a connector’s mating space:

board hole reaches clear wanted
fpga-audio H1 J1 0.42 mm 2.5 mm
pico-carrier H2 J1 0.53 mm 2.5 mm
motor-driver H2 J3 1.50 mm 2.5 mm
buck-5v H1 J1 1.58 mm 2.5 mm

A connector is judged by what plugs into it. A screw terminal’s wires leave horizontally from its face and a barrel jack swallows a plug the size of the jack again, so a screw beside either can only be driven before the cable goes on - which, on a board that gets serviced, is never. A pin header is the other case: the socket that fits it lands inside the outline the header already draws, so it asks for nothing beyond the screw head’s own room. Treating the two alike is what left a carrier lined with headers with two holes on one edge.

Three rules say all of this now - mechanical.fastener_clearance, mechanical.connector_access, mechanical.fastener_copper - and all three block a generated design.

Making room cost a second round. The first fix kept the clearance and lost the holes: the search slid along two edges from each corner and gave up, so boards that had had four came back with one. It walks a ring round the board now, at the inset the screw needs, taking the nearest free point to each corner - and only from that corner’s own quarter, because a search that asked for the nearest free point anywhere put three of the four along one edge, each of them the nearest thing to a different corner. Where a quarter has no room there is no screw: opamp-filter’s left edge is a screw terminal, a pin header and two resistors standing 6 mm in, and two holes that hold the board flat are the honest answer there.

The ink was measured a fifth short

H1 printed off the board for two reasons, and the second is the one that mattered elsewhere too.

The first: a designator that found nowhere to go kept the library’s position, and a library puts a mounting hole’s reference above the hole - which in a corner is above the board. Every silk placement is scored now, over a candidate set wide enough to have an answer, and the score counts ink past the outline as the worst thing it can do. silk.off_board reports it, because KiCad’s own test measures ink against the edge and says nothing at all about a string that clears it: ink past the outline is not trimmed, it is never printed, since the panel is routed at the line and the designator leaves with the offcut.

The second: the placement measured a string at 0.78 of the text size per character. KiCad’s stroke font is proportional and runs 0.94 to 1.07, measured on written boards with GetBoundingBox. A fifth under is the difference between a legend beside a capacitor and a legend across its land, which is what ADC_VREF was doing on the carrier - and what nothing caught, because the generator’s own check used the same short ruler as the placement it was checking. _text_extent takes the top of the measured range and adds the gap the fab wants; the review rule keeps its own short ruler on purpose, so it reports overlap it is sure of.

What came out of the pipeline behind them

Moving parts and re-routing three boards turned up three more:

What was left, and what it is

Two of the FPGA board’s findings survived the round and are waived with their measurements in gate.toml: three nets running 10 to 19 mm over cuts in the back plane, and one run that goes round at twice the straight line. They are the same fact twice. A 48-pin QFN on two layers has one signal layer for its pins to escape onto, so every pin that cannot get out on the front crosses on the back, and every back-layer crossing saws the plane under somebody’s return current. The router already prices a millimetre on the plane side at thirty on the front, and that number is a frontier rather than a preference: at forty the crossings stop and the tours start, and route.wander fires instead of route.return_path. Thirty is where the pair is smallest. What is left at thirty is what a second signal layer would buy.

_surfaced was written for exactly this and asks the question once more at the end - a short back-layer hop is offered the front against the finished board, because the router priced it before the rest of the board existed. On these five it lifts nothing: where the plane is cut, the front above it is full. It stays in the pipeline because it costs nothing when it finds nothing, and the next board may not be so tight.

24. The reviewer’s pass, round nineteen: the tab, and the first edition

Two things, and the first is a manufacturing defect the review round before had introduced without noticing.

The plane drank U1’s heat

Moving the fill to KiCad’s own filler changed what connect_pads thru_hole_only means. The generator had used it to mean “the plane ties on at the through-holes and the surface pads keep the track they were routed with” - which is what its own filler did. KiCad reads the same token as Reliefs for PTH: through-hole pads get spokes and surface pads get solid copper. Nothing said so, because the fill was correct by every rule the toolkit had.

U1 on the buck board is an LM2596 in TO-263, and its tab is 101.5 mm² on GND. Tied straight into the pour, it reaches solder temperature after the part’s own five leads do, and the part lifts on the leads that got there first. The reviewer saw it on the plot: a pad with no relief ring, merging into the plane.

The answer is not the zone. A chip capacitor’s land reflows with the whole board and a solid tie is the better electrical answer; the tab is a different object that happens to be drawn the same way. So the decision is per pad, and the pad carries it (zone_connect):

pad area vias in it connection
a 0805 land 1.45 mm² — solid, as before
C1.2, C3.2 (electrolytic grounds) 11.0 mm² 0 relief
U1.3 (TO-263 tab) 101.5 mm² 0 relief
fpga U1.49 (QFN exposed pad) 12.2 mm² 9 solid, and it already said so

The last row is the whole reason the rule needs a second clause. A via array in the pad is the board saying the copper is the heat path, and relieving it would be undoing the thermal design. layout.solid_pad_connection now reports the reflow case as well as the iron, with the same exemption.

Then the reviewer asked for the spokes to carry the current, not just release the heat - “パスを少し多くするか太くしたい”. KiCad draws four spokes and offers no way to ask for more, so the answer is width. Each relieved pad now sets its own thermal_bridge_width: half the widest track that reaches it, which puts twice the track’s own copper across the four, and for a tab with no track at all

Lifting a back-layer hop to the front then produced two width steps where the via used to be: a 0.2 mm hop between 0.4 mm runs, and a width change at a layer change is a change nobody reads while the same change mid-run is route.width_step. _surfaced widens a hop to match its neighbours when they agree, and leaves it on the back when they do not.

The middle column

The examples compared two variants, and the left one had quietly stopped being a fair “before”. Eighteen rounds of findings went into the generator, not into patches on its output, so as-generated improved every round without anyone reviewing it. The comparison understated what the review had been worth.

So each comparison has three columns now, and the leftmost is each design as it came out of the generator the day it was written - recovered from this repository’s own history, one git show per file, rendered with today’s renderer so the only difference is the design:

design first edition as-generated reviewed
buck-5v 45 blocking 28 blocking PASS
motor-driver 43 37 PASS
pico-carrier 50 33 PASS
opamp-filter 33 32 PASS
fpga-audio 34 29 PASS

The distance between the first two columns is the review turned into code, which arrives before anyone runs the gate. The distance between the second and the third is what the gate still had to catch on the day.

25. The reviewer’s pass, round twenty: floorplan before routing

The previous rounds taught the router to make poor placement look orderly. That was useful, but it left the decision in the wrong place: once two parts that must talk are thirty millimetres apart, no choice of 45-degree corners can make the floorplan compact.

layout.connection_span now asks the question before copper exists. For each non-ground net it collapses all pads on one footprint into one node, computes the nearest pad-to-pad distance between every pair of footprints, and builds a deterministic minimum spanning tree. Any edge over max_connection_span_mm (25 mm by default) is a placement finding. The use of a tree matters: a three-pin net needs two local connections, not every possible pair, and two far-apart pads inside one package must not accuse the package of being far from itself. The ai-generated policy promotes the rule to an error; a long backplane can raise the threshold in its own policy.

Running that rule over the old reviewed baselines found 2 long logical hops on buck-5v, 10 on motor-driver and 13 on fpga-audio. It found none on opamp-filter or pico-carrier, which is also useful evidence: a rule intended to judge placement did not merely rediscover dense routing everywhere. Rebuilding the three floorplans took all 25 findings to zero:

design old baseline rebuilt baseline consequence
buck-5v 126 × 56 mm, 459.30 mm tracks, 124 vias 92 × 38 mm, 284.04 mm, 87 vias input switch loop and output filter become one power-flow row
motor-driver 88 × 50 mm, 654.06 mm tracks, 94 vias 68 × 46 mm, four layers, 472.54 mm, 89 vias supply capacitors share the package fan; logic header meets its lanes; In1 is continuous GND and In2 is VM
pico-carrier 88 × 62 mm, 90 vias 80 × 60 mm, 78 vias edge clearances retained while unused perimeter is removed
opamp-filter 58 × 42 mm 58 × 42 mm a smaller trial made the analogue feedback routing worse, so the honest optimum stayed put
fpga-audio 100 × 84 mm, two layers 76 × 58 mm, four layers the QFN gets a solid In1 reference and In2 +3V3 plane; return-path and routing-tour waivers disappear

The FPGA change is not a concession to the gate. It is the engineering result the gate exposed. A four-sided QFN-48, codec and boot flash can be forced onto two layers, but the resulting cuts in the only reference plane are not a good teaching baseline. The rebuilt board spends two inner layers on an uninterrupted ground reference and power distribution, leaving the outer layers for escape and signals. One SPI clock lane crosses the In2 pour, still referenced to In1; the +3V3 plane remains continuous around it rather than being mistaken for the signal’s reference.

That board also exposed one parser assumption: KiCad writes a through via as F.Cu B.Cu on a four-layer board. Those are the barrel’s endpoints, not the only copper layers it reaches. The stub and via-in-pad checks now expand the inclusive layer range; a regression test puts an In2 track into a through via so the mistake cannot return.

The cold KiCad passes found final defects that a no-CLI review could not close. Buck-5v’s output electrolytic overlapped J2’s assembly courtyard even though their copper was legal, so C3 moved 1.5 mm toward the inductor. On the motor board, D2 moved 2 mm into the space between R2 and C1 to clear both courtyards. Route straightening had also moved AIN2’s first via back across its fixed fan, leaving a 0-degree foldback; that escape now stops at its declared 45-degree fan exit and stays on B.Cu until the through-hole header, instead of returning to the front above the cuts made by the other control lanes. None is waived: manufacturability, a non-self-reversing route and a local return path are properties of the baseline.

Finally, regeneration itself is a gate. CI now rebuilds all five projects from an empty route cache inside the pinned KiCad 9 image, diffs every schematic, project and board file, runs each reviewed policy, and renders both drawings. The generated projects, verdicts and renders are uploaded together. A checked-in example therefore has to be reproducible as well as electrically and geometrically acceptable; a hand-edited golden file can no longer drift away from the generator that claims to own it.

26. Follow-up audit: circuit requirements and reproducible evidence

The previous green CI run did not establish all the claims made about the designs. This pass rechecked the selected components, actual layer ordering, cache semantics and the cost of the new placement rule.

Motor driver: fix the circuit and the placement

The selected part is DRV8833PWR, the PW TSSOP package without an exposed thermal pad. Its stated rating is 0.5 A RMS per bridge at VM = 5 V and 25 °C; the 1.5 A headline applies to thermally enhanced packages. C2 changes from 100 nF to 10 µF for VM, C4 from 1 µF to 2.2 µF for VINT, and C3 remains 10 nF between VCP and VM. R1 no longer uses VINT as an unspecified logic supply: the host must pull J4.7 nFAULT up to 3.3 V through 10 kΩ. Grounded AISEN/BISEN disable PWM current regulation; the internal overcurrent trip is fault protection, not regulation at the PW package’s continuous-current rating. TI DRV8833 datasheet, SLVSAR1E.

The former 11.7–11.9 mm bypass distance was not an inevitable consequence of 0.65 mm pin pitch. The redesigned east side puts C2/C3/C4 beside their supply pins; logic drops to B.Cu at the package, and the IC grounds drop directly to In1. VM reaches each local capacitor from In2, without a redundant long outer-layer trunk. The decoupling-distance waiver is deleted: the normal 5 mm rule is now part of the acceptance criterion. The regenerated board’s pad-centre distances are 2.69 mm (VM–C2), 2.88 mm (VINT–C4) and 3.37 mm (VCP–C3). These are the placement rule’s measurements, not measurements of loop inductance or a substitute for power-integrity work.

These changes do not certify the motor supply, wiring or thermal design. Effective ceramic capacitance at operating bias, transient/reverse-polarity protection, motor stall behavior and emissions still need application-specific assessment. The current rating is conditional, not a guarantee for any ambient temperature or enclosure.

Plane presence is not return-path verification

Both four-layer examples use F.Cu / In1 GND / In2 power / B.Cu. In2 is adjacent to B.Cu, not In1. The FPGA’s SPI clock also cuts a lane through In2. The existing route.return_path rule only checks two-layer boards, so removing its waivers did not prove multilayer reference continuity. CI now renders individual copper layers and checks that In1 has no foreign signal tracks and retains a single filled GND region covering at least 90% of its zone outline. That is a baseline regression guard, not an SI/EMC or impedance criterion. The real dielectric stack-up and reference transitions remain review items. The FPGA’s 16 decoupling-distance exceptions also remain open design work. Their wording no longer claims that a better single-sided placement is impossible. Its former assumed 60 mA budget is likewise not a firmware-specific power analysis. These retained demonstration exceptions are not production sign-off, even when all automated acceptance checks pass.

Gates that test the stated requirements

tools/check_example_contracts.py checks the motor component values, package rating and exact VINT/VCP/nFAULT connections on both schematic and board, alongside the four-layer GND contract. It also rejects a skipped schematic or board stage, unavailable ERC/DRC and crashed review rules (even when reported as info or waived), and requires each negative control to fail for the intended title/rating/part-number defects. An unrelated error is not sufficient proof that a negative control is still useful. Reviewed goldens also reject unwaived native DRC warnings, even if a policy demotes their severity. KiCad 9 exposed a redundant FPGA +1V2 via (a B.Cu-to-B.Cu bend, not a layer transition) and a buck output legend touching C3’s body silk. The via is removed and the legend has an explicit clear position; neither diagnostic is dismissed as harmless simply because the generic gate passed. The motor rebuild also exposed an incorrect inner-power-zone name: VM instead of the schematic/pad net /VM. KiCad treated the plane as a different net; the former outer-layer trunk masked the missing power-plane connection. Pad and zone names now use one canonical naming helper, with regression tests for root labels and power symbols. The project contract checks the exact In2 net name rather than normalizing away the distinguishing slash.

Route caching previously discarded Track.keep_layer; the loop-removal and run-merging passes could also lose it on a cold run. All now preserve that intent. Cache version 2 invalidates older entries, and its key includes whole footprint definitions so an unconnected pad’s size change invalidates routing too. CI requires a cold run and a cache-hit run to produce byte-identical projects. Generation, gates and renders all use pinned KiCad 9; the former workflow accidentally gated and rendered with the global KiCad 10 default. The version-matrix jobs also gate all five checked-in reviewed examples and apply their contracts, rather than relying on the small test fixture to prove example compatibility with both KiCad versions. Their JSON verdicts are uploaded separately for each version. Golden regeneration runs in five independent matrix jobs with fail-fast disabled. Each retains its own cold/warm comparison, both verdicts, contract checks and renders; one failed design cannot hide the other four results.

Finally, layout.connection_span uses a maintained Prim frontier: O(n²) footprint-pair evaluations instead of rescanning every cut in O(n³). Pad-pair distance and deterministic tie behavior are retained. Regression tests check both fixed-layer preservation and operation counts, so a passing small example cannot hide the previous large-net performance problem.

An independent comparison with the previous implementation returned identical edge lists on 200 seeded, randomized multi-pad/tied-distance cases. On the same host, a 500-footprint single-pad net took 16.557 s before and 0.288 s after. These are illustrative timings; CI enforces operation counts and correctness, not a wall-clock threshold tied to one machine.

27. The reviewer’s pass, round twenty-one: the parts a bench never asks for

Nineteen rounds had made five boards that pass their own gate, and §1 still listed five things a production review would ask about that no rule did: nothing between any screw terminal and the circuit behind it, an output capacitor whose ESR the regulator’s loop depends on and nothing states, a clock leaving its oscillator with nothing to damp it, a DAC that un-mutes with the rail, and motor leads that leave unfiltered. This round redesigned the circuits for four of them and asked, for each, the question the whole set exists to answer: what does the rule look like, and does the netlist carry enough to write it.

What changed in the designs

What went into the tool

Three rules, each written from what the netlist alone can carry:

rule what it reads fires on the demo corpus
analog.unprotected_power_input a connector of four pins or fewer with a ground and a supply and nothing else is where power comes on. From its supply pin the rule walks inward through two-terminal series parts - fuse, diode, inductor, bead - and asks whether it passed a fuse and whether a diode stands in the path or across it. A rail an output or power_out pin drives is the board’s own, and a connector on it is an output once (multichannel, a 12 V terminal with nothing behind it)
analog.clock_no_series_resistor an oscillator module’s output pin on a net with no resistor once (tiny_tapeout)
spec.missing_esr a polarised capacitor on a net an inductor also reaches - a switching regulator’s output - with no ESR field 18, fourteen of them one design’s row of electrolytics, graded info and collapsed

The first needed the walk. A fuse is not on the rail the IC sees - it is one net upstream - and a Schottky in series is as much reverse-polarity protection as a TVS across, so the rule follows the supply through whatever two-terminal parts it passes and judges what it collected. The buck’s own output terminal is what made the direction test necessary: a 5 V screw terminal with a ground and a supply and no fuse looks exactly like an unprotected input until the walk reaches, one inductor away, the pin that drives it.

What the tool then made us fix

All five as-generated variants now carry one finding more than they did - spec.missing_esr on the buck’s C3, because degrade strips every field - and the reviewed variants had to earn their new parts:

Where the five stand

design as-generated reviewed
buck-5v 32 blocking PASS
motor-driver 33 PASS
pico-carrier 27 PASS
opamp-filter 38 PASS
fpga-audio 34 PASS

The waivers are the ones the rounds before left, unchanged in number and in reason. The Datasheet fields on the new parts are the TVS link KiCad’s own Diode library carries for the SMAJ series and the fuse makers’ family pages; this round’s network reached the TI datasheet the ESR field is derived from and nothing else, so those pages are cited, not read, and the ratings on the sheets are the ones the design asks for rather than numbers copied from a table.

28. The reviewer’s pass, round twenty-two: what a layer costs

Rounds twenty and twenty-one were written against the same baseline by two different hands, and both landed. Merging them was the first half of this round; the second half was a question the merge exposed.

Round twenty had answered two findings — the motor driver’s return path and the FPGA’s reference plane — by moving both boards to four copper layers. That is a real answer, and on a board that needs it, the right one. It is also the single most expensive change either board could have made. A two-layer prototype run is priced as a commodity; a four-layer run is not, and the difference on the motor driver exceeded its entire bill of materials. Nothing about a DRV8833 and five capacitors asks for an inner plane.

So both boards went back to two layers, and the round is about what that cost and what it did not.

It cost less than the four-layer answer implied

The motor driver kept round twenty’s floorplan exactly — 68 × 46 mm, the bypass capacitors hard against the package, every layout.connection_span and layout.decoupling_distance finding still at zero. Only the supply changed: with no In2 plane to disappear into, VM is a stated front-side spine down the free column right of the capacitors, threaded between the two ground vias at x = 45.15. The gap between their barrels is 1.7 mm and the arm needs 0.9 of it.

What is left is one finding, and it is now a waiver with both measurements in it: route.return_path at 12.8 mm and 10.4 mm against a 10 mm limit, where the four logic lanes cross under the four bridge outputs on the back. The waiver says what a design running those lanes faster should do instead of copying it.

The FPGA board is the honest half of the trade. A 48-pin QFN, a codec, a boot flash and an oscillator escaped on two layers do not fit in 76 × 58 mm; the board is 100 × 84 mm, and that is the shape of paying in area rather than in layers.

Three rules came out of it

Taking the planes away made two questions worth asking of every board, and the merge made a third overdue.

layout.pour_edge_cut walks the ground pour’s own outline, half a millimetre inside it, and asks whether the fill still reaches all the way round. The outer ring is the board’s outermost copper: the shield the edge radiates into, the return every edge-hugging track leans on, and part of what a fabricator reads as copper balance when it plates the panel. Broken, the two halves of the rim meet only by going the long way round through the middle of the plane — which is the loop the rim was closing. Things are allowed to interrupt it: a mounting hole and its clearance, a through-hole land at the edge, the board’s own outline where it steps. A route is not, so the rule only reports a gap with a foreign track or via standing in it.

It reports as a warning and blocks under ai-generated. Five of KiCad’s own eighteen demo boards trip it, and those boards ship: a nibbled rim is a thing a human judges, not a thing that is broken on its face. On a board this repository generates there is nothing to judge - the fix is to move the route inboard, it costs nobody a respin, and the policy makes it an error.

route.via_under_package is a warning, and route.under_package grew to cover connectors as well as chips. Under an integrated circuit there is no plane between the copper and the die and no way to probe or rework it; under a connector the shell has to come off before anyone can even see it, which is why the connector case measures against the courtyard rather than the pad box. Both exempt the part’s own nets — its escapes and its ground stitching belong there — and the via rule also exempts a thermal via inside the part’s own pad, which is what an exposed pad exists to have.

The connector case earned itself immediately. On opamp-filter the 5 V rail was cutting the corner off J2’s courtyard on its way to the second amplifier: the short way across, and copper nobody could have probed. The strip under the terminal’s body is now fenced and the rail goes round it.

One hole drilled twice

Moving the FPGA board onto two layers put every layer change on the same pair of faces, and the search spends a via at each end of a hop. Two hops that turned round within half a millimetre of each other got a barrel apiece: 0.5 mm between centres, 0.1 mm between the holes, against the 0.2495 mm a fabricator here will place. KiCad’s own DRC reported it as hole_to_hole — the gate did not, because this is a manufacturing constraint the board setup carries rather than a rule this toolkit writes, which is why CI checks both.

The pair is one hole on one net whose copper already overlaps, so the generator now merges it. _uncrowded runs after the copper has stopped moving, finds same-net vias closer together than a drill will go, and puts one via at the centre of every track end the two were serving — but only if that one still reaches all of them. A via anchored to a pad never moves: it was placed beside that pad on purpose.

What the cold route said that the cached one did not

The rule was not enough. route.under_package was clean on the board this checkout held, and the golden CI job — which routes from scratch, with none of the rip-up order an afternoon of attempts had learned — found a different solution and put +3V3 under both headers, nine segments of it. The property was luck, not design: the same board, routed twice, was clean once.

route_keepout could not have caught it. It closes the strip between two rows of pads, and a 1×N header has one row, so J2 and J3 were never fenced at all. What a connector needs is its whole courtyard closed — to every net but its own, because its escapes still have to leave. An obstacle could not say that: it carries one net, and a part has as many nets as it has pins. So Obstacle grew open_to, a set of nets it does not block, and Design.body_keepout names the parts whose courtyard is fenced that way. The FPGA board fences its two headers, and the rail goes round them.

The cold route is the one that counts. A board is only reproducible if the copper checked in here is what the router finds with an empty cache, because that is what CI regenerates and compares against — the learned order makes the answer arrive sooner, never differently enough to commit.

Who pays for a tour

The reviewer circled a column of ten vias on the motor driver, between the package’s escape fan and the two motor terminals, and asked for a principle: route the nets with something to lose first — current, a clock, a bus, a pair — and never let the miscellaneous routing add stubs and vias to them; place the parts so those routes are easy.

The column was exactly that inversion. The four 0.4 mm bridge outputs run a clear corridor west to the terminals. One 0.3 mm logic input, AIN1, with both ends on the package’s east side, found its straight lane taken and toured the whole west end of the board instead — and the chase for tidiness then promoted it to the front, so the outputs, routed after it, hopped under it: two vias apiece, and nFAULT the same. The tour itself had two causes. The design had put AIN1’s drop on the back on purpose, and the search still charged it the plane surcharge — thirty on the front for a millimetre on the back — so seventeen millimetres on B.Cu cost more than seventy-five on F.Cu with two vias. And its header pin sat on the far side of AIN2’s stated lane from its via, so it could not have dropped straight even for free: the header’s pin order did not match the order the drops arrive in.

Three things changed. The routing order now has two classes: a link wider than the board’s thinnest, or on a net the design names in priority_nets, is routed while the board is empty, and a failure or a tour promotes a plain link only to the front of the plain links — it never moves ahead of a net with a claim. A link the design declares on the back and keeps there pays the router’s ordinary rate, not the plane surcharge. And J4’s pins were reordered so the four drops leave the via column in the order they land — GND, nSLEEP, AIN2, BIN1, AIN1, BIN2, nFAULT, GND — with AIN2’s lane turning where its last leg to the new pin is a 45.

Cold, under CI’s conditions, the motor board now routes on the first pass with no rip-up and no chase. The four outputs and nFAULT: zero vias, all on the front. AIN1 crosses BIN2’s drop and pays two vias for it, which is the plain net paying.

The op-amp filter said what the width heuristic cannot: the filter’s own signal path is at signal width, and routed after the rail’s links its two filter nodes toured 6.5x and 7.7x under both terminals. On a filter that path is what the board is for, so the design names it — IN, IN_DC, X, FILT_IN, OUT, VREF — and the bias divider’s midpoint and the output coupling are the plain links that go round. Its rail, routed first, then took the short way under the input terminal’s shell; the three connectors are fenced whole now, the way the FPGA board’s headers are. The FPGA board says the same of its rails: +3V3 and +1V2 are distributed at signal width there (the track.thin_power waiver is about exactly that), and routed after the clocks and the bus the codec’s own supply pickup had no lane left between the package and the jack. Named, they route first, as a rail should.

That board also found the classes’ limit. XSMT — the mute line, whose only possible seat for R6 the detour waiver already describes — has no lane at all behind the priority nets, and neither has LDOO, the codec’s regulator output into its reservoir. Feasibility is the hard constraint and the classes are not: a plain link that fails from the front of its class is lifted ahead of the priority nets, and the log says so — three times on this board, both XSMT links and LDOO. That is a floorplan with no room for them, and the placement’s problem to fix; it is not a reason to call the nets special.

Two names over every other pin

The reviewer then read the motor driver’s header silk and could not tell which name went with which pin. The placer had been clearing each legend of its neighbour by sliding it along the row, up to a whole pitch — and a whole pitch on a 2.54 mm header is the next pin. The lower row of legends had each moved one pin along, so BIN2 printed under nFAULT’s pin, BIN1 under AIN1’s, nSLEEP under AIN2’s: two names over every other pin, and nothing to say which was whose.

A legend names the pin nearest to it, so it may slide only while the pin it names is still that: not at all between the pins of a 2.54 mm header, half a pitch either way on a 5 mm terminal block, and as far as it likes past the end of a row, where there is no other pin to name. A label that does not fit beside its neighbour goes to the other side of the row instead, still on its pin. All 73 connector legends on the five boards now sit nearer the pin they name than any other.

What that rule could no longer hide, it exposed. On the Pico carrier a 22 uF capacitor stood in the header’s legend strip across the rows of pins 3 and 4, and a 100 nF across pin 6’s — the legend ADC_VREF, the longest name on the board — so those legends had been printing one pin along to avoid them, and with the slide gone they printed on the capacitors’ pads instead. The capacitors moved out of the strip; the review had been reporting the wrong pin’s name over the right pin as clean silk.

A pinout is read down a column

Two more things the reviewer asked of the silk. A connector’s pin names should line up — the same side, the same distance, and turned to suit — so their positions can be read at a glance; and no designator or description should stand where a fitted part will hide it.

The legends are now laid out a row at a time. Every pin of a connector gets the same side and the same distance from the pad row, anchored on its own pin; where the names are wider than the pitch, as on any 2.54 mm header, they turn a quarter and stand up from their pins in one line. Only a row that cannot be lined up clean — the Pico carrier’s supply terminal, with the fuse in its strip and the board’s edge on the other side — falls back to placing each pin on its own, and then a legend steps along the row only as far as still names its pin.

For the hiding, the placer had been weighing another part’s courtyard as merely close — a grazed outline cost a fraction of what a covered pad did — and so the motor driver’s fuse had its name a millimetre inside the bulk capacitor’s outline, and the op-amp board’s own name ran across a test point standing in the strip the name is written in. Other parts’ bodies now weigh as much as pads for every string the generator places, the test point moved out of the name’s strip, and silk.under_part reports whatever slips through: a string inside a foreign courtyard on its own side. The review’s own estimate of a string’s extent learned where its anchor is at the same time — a legend anchored at the end nearest its pin had been measured as if centred, half a string away from where it prints.

The measurement that was answering the wrong question

The round above reported seventy-two legends “on their own pins” and two boards clean of hidden ink. Both numbers were true and neither answered the question. The reviewer came back with three screenshots, and what they circled was VIN printed beside the buck converter’s fuse, GND beside the motor driver’s clamp, +5V beside the Pico carrier’s fuse, and C1 printed between the two pads of the capacitor it names.

The legend check had compared each legend against its own connector’s pins. Every one of them was nearest its own pin among those — and two millimetres from a fuse’s pad belonging to somebody else. The hidden-ink rule had exempted a string inside its own footprint’s courtyard, on the grounds that a designator beside its own part is the convention. It is, but a courtyard is the part plus the room to place it, and the exemption was covering the case where the part itself stands on the name.

Measured against every pad on the board, and against each part’s own fabrication outline rather than its courtyard, all five boards were reporting:

board legends naming another part’s pad strings under their own part
buck-5v VIN, GND, +5V C1, C3, L1
motor-driver GND J2, U1
pico-carrier +5V, GND, 3V3_EN F1, U1
opamp-filter VIN, GND, OUT_AC J2
fpga-audio GND ×2, OUTL, OUTR U2, U4

Two rules now ask those questions instead. silk.under_part keeps the courtyard test for a neighbour and adds the fabrication outline for a string’s own part — the two differ on purpose, and the difference is exactly the margin a designator beside a chip resistor lives in. silk.pin_legend takes any string naming a net a connector carries, within 15 mm of a pad of that net, and asks whether the nearest pad on the board carries that net.

Where a name cannot sit on its pin

The designators were the easy half: step the name outside its own part’s fabrication outline, measuring what that takes per direction rather than as one radius. A footprint is anchored where its library chose to anchor it, which for a screw terminal is pin 1 and not the middle of its shell, so a single radius big enough to clear the far side puts the name three millimetres past the near side and into the next part.

The legends have no such answer. A supply terminal at the edge of a board has nowhere to be labelled: outboard is where the wire goes in, inboard is the fuse and the clamp that every supply input in these examples now carries, and the reviewer’s suggestion is the only thing left — write the name where it can be read, box it, and point at the pin with straight lines bent at 45°.

That is what the generator does when, and only when, no placement puts the legend’s own pad nearest to it. The label goes in a frame, so it reads as a label rather than as the name of whatever it is standing beside; a leader runs from the frame back to the pad in horizontal, vertical and 45° legs. Spots are tried outward from the pin and the first whose frame and leader are both clear wins, so the leader stays short.

Four details decide whether it reads.

The leader is drawn from where it comes out of the connector, not from the pad: a screw terminal’s pads are under its shell, ink there is invisible and silk_overlap besides, and the side the line emerges from is what tells a reader which pin it came from. A direction that would take it through another pad on the way out is refused outright — the motor driver’s terminal had both its names pointing at the same spot on the bottom edge of the shell, because the upper pin’s leader had gone down through the lower pin to get there and nothing visible said so.

The choice is ranked rather than summed: what the drawing takes from the rest of the board first, then whether the leader is long enough to read as a pointer rather than a tick, then nearest to the pin. Summed, a two-millimetre penalty for a short leader sent a label twelve millimetres away to buy one it could have had at four.

A line has no area, and the placer scores by area. A leader drawn straight across another part’s outline cost exactly nothing, so it did it, four times on the Pico carrier. Every drawn line an obstacle now has the width it is drawn at, plus its clearance.

And the relocation happens at all only where the name is on its own. Where three or more of a connector’s names sit at one offset from their own pins, they are a column: the third name down belongs to the third pin whatever else is nearby, which is the whole reason for lining them up, and taking one out of the line to point at its own pin makes the pinout worse rather than better. Four of the Pico carrier’s forty header names have a bypass capacitor’s pad marginally nearer than their own pin, and all forty read fine. That test is asked of where the names ended up rather than of what the placer attempted: one pin with a fiducial in its strip sends a twenty-pin row to per-pin placement, and nineteen of them still land in one line. Two names side by side are not a column, which is why the supply terminals still get leaders. silk.pin_legend carries the same three clauses, so the rule and the generator agree about what a readable pinout is.

One more clause turned out to matter more than the mechanism: the test is the net on the nearest pad, not the pad. VIN printed between a terminal’s pin and the fuse pad that pin feeds names both of them, and both are VIN - nothing is wrong with it, and an earlier version that compared pads rather than nets sent four such legends off to find leaders they did not need.

Twelve of the seventy-seven pin legends across the five boards are drawn with a leader: three on the buck converter, four on the op-amp board, two each on the motor driver and the Pico carrier, one on the FPGA board. The rest sit against their pins, which is better and is still what is tried first.

Graded against KiCad’s own demo boards, silk.pin_legend fires seven times on three of the eighteen, which is what a rule about one specific mistake should look like. silk.under_part fires 277 times on twelve of them, up from eleven before the own-outline clause, and nearly every one of those is a designator left where its library drew it. That is a habit on a hand-laid board and a defect on a generated one, so it stays a warning that the ai-generated policy promotes rather than an error everybody trips over.

A loop that never existed

The op-amp board’s new copper also found a defect in the loop cutter. MID had a stub from a pad up to a via and the back-side run coming down from that via, passing under the pad on its way. The cutter splits a run at a pad of its own net it passes over, because the overlap feeds the pad — but it split the back-side run at a front-side pad, manufactured a node there, and the stub ending on that pad then shared it: a cycle. It cut the stub and the via and left the run starting at the pad on the wrong layer, with no way up to it — two of KiCad’s unconnected_items. The split now respects the pad’s layer, and the test runs the same copper through the old cutter to show the via go.

The smallest change in the round

Fiducial designators no longer print. A fiducial names a target the assembly machine finds optically; nobody reads its designator on a bare board, and on a 68 mm board it was competing with the board’s own name for the same edge strip — silk.text_over_text, on the one string that could have been deleted instead of moved.