Known-fault validation
The full record. It is published here rather than in the tree: the hunt it came out of examines a board that is not public. File paths and test names in the text point into the repository.
Before trusting hauksbee to find unknown bugs on famous boards (the
docs/evidence/FAMOUS_SWEEP.md campaign), we calibrate it against boards with known
faults, specifically faults that were fixed between two public design revisions.
The revision history is ground truth: "hauksbee flags revision N for exactly the
thing revision N+1 fixed" is the strongest calibration the tool can have, and a
documented fix that hauksbee misses tells us, with citation, where its reach ends.
This is governed by the same project meta-lesson (docs/evidence/BUG_HUNT.md): a
surprising result is presumed a defect in OUR tool until chased to ground truth.
Here it cuts the other way too: a tempting "hauksbee could catch this" was
measured against the clean corpus and rejected when it would have manufactured
false positives. The honest negative is part of the calibration.
Method
For each documented fault we:
- Find the prior-art citation (changelog entry, issue, PR, errata, handbook).
- Classify it: in scope for hauksbee (electrical connectivity, missing pulls, component ratings/stress, undefined power-up states, copper shorts/clearance, regulator derating) vs out of scope (RF, mechanical, firmware, EMC, transient thermal).
- For in-scope faults, obtain the FAULTY and FIXED revision design files and run
hauksbee's toolchain (
--report,--drc,--lint, headless stress) on both, recording one of three honest outcomes:- FLAGGED (gold row): hauksbee flags the faulty revision for the fault and goes clean on the fixed revision. Encoded as a CI test where the corpus files allow.
- MISSED, check-class exists: improve the check to catch the real fault without manufacturing false positives elsewhere (re-run the famous-corpus clean sweep to prove no regression), or, if that is impossible, record exactly why and keep the honest miss.
- MISSED, out of reach: state precisely what hauksbee would need to model.
Two revision pairs carry this comparison. The upstream, pinned revision and
licence of every fetchable board is corpus.toml, which scripts/fetch-corpus.sh
reads:
zswatch_devkitv1.1.0, v1.2.0, v1.2.1 (ZSWatch Watch-DevKit-HW, GPL-3.0)watchy_historyv1.5, v2.0 (Watchy-Hardware, MIT)
Which of those you can actually obtain. corpus.toml pins ZSWatch DevKit
1.2.0 and 1.2.1 and both Watchy revisions, so a plain fetch-corpus.sh gets
them. It does not pin DevKit 1.1.0, and the DISPLAY-EN row below is the
1.1.0-to-1.2.0 comparison, so that row's faulty side is held only in the
maintainers' corpus and is not reproducible from this repository today. The tag
exists upstream under the same GPL-3.0 terms as the revisions we do pin, so this
is a gap in corpus.toml rather than a licensing obstacle; adding it would make
the row reproducible.
corpus.toml also carries a "Local-only boards" section listing what is
deliberately absent and why, including reconstructions that are not ours to
redistribute. When a row below cites one of those, it is evidence you can read
but not re-run.
The native design files for the other boards in the matrix (Olimex ESP32-EVB rev
A..L, MNT Reform motherboard 2.0/2.5/3.0, ZSWatch mainboard, Watchy v3) come from
the famous sweep and are all pinned in corpus.toml.
Fault matrix
| Board | Rev pair (faulty -> fixed) | Documented fault | Source | In scope | hauksbee check | Outcome |
|---|---|---|---|---|---|---|
| ZSWatch Watch-DevKit-HW | 1.2.0 -> 1.2.1 | Missing pull-ups on the RTC-side I2C bus (PCA9306 SDA2/SCL2 -> RV-8263) | PR/issue #158; CHANGELOG 1.2.1 | Yes (missing-pull) | missing_i2c_pullup |
FLAGGED (gold row) |
| Watchy | v1.5 -> v2.0 | e-paper RES# on a Hi-Z-capable GPIO, no pull-up; display will not enter deep sleep | issue #14; PR #17 | Yes (undefined power-up state) | boot-coverage (firmware co-sim) |
EXECUTED (Round 5): boot-coverage run on the real Watchy v1.5 board under the Espressif QEMU ESP32 backend, two-sided GREEN/RED. The display-init firmware drives RES (GPIO9) HIGH a few hundred ms into an 800 ms run, well inside the assertion's 700 ms deadline (PASS); a firmware that never touches GPIO9 leaves RES at 0.000 V and never reaches 2.6 V (FAIL). Reduced firmware, labelled honestly (see below). |
| ZSWatch Watch-DevKit-HW | 1.1.0 -> 1.2.0 | DISPLAY-EN (NTS0104 OE) on a Hi-Z-capable GPIO, no pull-up; display undefined at power-up | issue #123 | Yes (undefined power-up state) | (control-pin class) | MISSED, not structurally separable (decidable with firmware co-sim; ZSWatch's nRF5340 has no backend and cannot cheaply get one, because no nRF5340 platform exists in Renode or on its master and renode-infrastructure has no SPU model, no nRF53 IPC model and nothing for the network core, so there is nothing to vendor the way RP2040's models were. nRF52840 is the closest proven Nordic part; see docs/cosim/MCU.md) |
| Olimex ESP32-EVB | rev D -> rev E | Ethernet PHY 50 MHz REF_CLK on GPIO0 (a strapping pin) present at reset; ESP32 randomly enters download mode | Olimex HARDWARE changelog, rev E; ESP-IDF Ethernet API | Yes (undefined power-up state / strapping) | strap_pin |
FLAGGED (caught; honest reach limit: fires on every rev incl. the fixed ones, the rev-E fix is not netlist-visible) |
| MNT Reform motherboard | 2.5 -> 3.0 | LTC4020 ILIMIT pin unprogrammed; charger over-draws (75-88 W from a 60 W brick) | Handbook, Revised Versions; GitLab #19 | Yes (component-rating / missing-program-resistor) | LTC4020 behavioural converter (ltc4020_overdraws_on_mb25_and_is_clean_on_mb30) |
FLAGGED (gold row) |
| MNT Reform motherboard | 2.0 -> 2.5 | LTC4020 RNG/SS toggled to gate charge current destabilises the same IC's system DC/DC | community.mnt.re #177 | Yes (power-sequencing) | LTC4020 behavioural FSM (ltc4020_rng_ss_destabilises_only_when_driven) |
FLAGGED (gold row) |
| MNT Reform motherboard | 2.0 -> 2.5 | Current leak through the LTC6803-4 cell-balancing network when a cell position is empty | Handbook, Revised Versions | Yes (reverse-current) | LTC6803-4 behavioural leak law (ltc6803_leaks_on_mb20_and_is_clean_on_mb25) |
FLAGGED (gold row) |
| ZSWatch Watch-DevKit-HW | 1.2.0 -> 1.2.1 | nPM1300 SHPHLD internal pull-up to VSYS also wired to an MCU GPIO -> VSYS feeds the GPIO in sleep | issue #155 | Yes (over-voltage / reverse path) | nPM1300 behavioural internal pull (npm1300_shphld_feeds_gpio_on_120_and_is_clean_on_121) |
FLAGGED (gold row) |
| ODrive v2 inverter | Inverter45attempt.PcbDoc -> Inverter.PcbDoc (sibling layouts, same pinned rev) | GND <-> AGND copper overlap on F.Cu at x=153.2, y=183.9 (gap -1.0 mm), forbidden by the file's own rule set (only ShortCircuit rule is the Altium default, scope All/All, ALLOWED=FALSE, so Altium's DRC flags the same overlap; no scoped allowance sanctions a ground join) | The design files themselves: both layouts at ODriveHardware 079f30ac, v2/; the "faulty -> fixed" ground truth is the repo shipping the final Inverter.PcbDoc short-clean beside the attempt, not a changelog citation | Yes (copper short) | Altium DRC shorts (odrive_v2_attempt_ground_short_flagged_and_final_is_clean) |
FLAGGED (gold row); found live 2026-08-08 when the corpus fetch first materialised the file, adjudicated same day (odrive_v2 is known_good = false so the silence sweep defers to this row) |
| MWGEN-G1 signal generator | single revision (no revision pair: the pin is the upstream head as fetched, and no later fix was looked for) | Six pad-to-pad copper overlaps on F.Cu: D503 (SMAJ48CA TVS in an SMA body, 2.5 x 1.8 mm pads) over D203 pad 1 (GND <-> REFOUT_SEC, -0.150 mm at x=144.24, y=90.60), over D202 and D203 pad 3 (REFIN_GND <-> Net-(D202-Pad3), -0.060 mm at x=140.09 and x=142.36) and over the inner pads of R204 and C205 (REFIN_GND <-> REFIN, -0.015 mm at x=140.09 and x=141.75); J206's Laird BMI-S-205-F fence pad over FB204 pad 1 (GND <-> Net-(FB204-Pad1)) | The design file itself at MWGEN-G1 2fc77c90; ground truth is that the design's own rule set forbids all six (MWGEN-G1.kicad_pro sets shorting_items and clearance to error with an empty drc_exclusions) and KiCad 9.0.3's own kicad-cli pcb drc on the same file reports the same six shorting_items violations, same net pairs, same pad anchors |
Yes (copper short) | KiCad DRC shorts (mwgen_g1_pad_overlap_shorts_match_kicads_own_drc) |
FLAGGED (gold row); found live 2026-08-09 by the external-pool iteration qc/evidence/runs/2026-08-09-external-01.json, adjudicated same day, and pinned into corpus.toml as known_good = false so this row owns the file |
| OpenEnergyMonitor emonTx V3.4.5 | single revision (fabrication output as the oracle) | Reported GND <-> AGND shorts on BOTH copper layers at x=34.41, y=43.20, from the same-rank overlap of the two pour outlines | The design file, its shipped gerbers and its schematic at EmonTx3 411de961, hardware/V3.4.5/; ground truth is net attribution over Eagle's own CAM output (a filled region holding vias of both nets, on each layer) plus the .sch segment tying the AGND supply symbol to the GND one |
Yes (copper short) | Eagle pour overlap (emontx_ground_tie_is_real_copper_on_both_layers_and_both_are_reported) |
TRUE ABOUT THE COPPER; SCHEMATIC INTENT IS CONTEXT ONLY: both layers really do join the two nets and the schematic declares the pair, but carries no board coordinate that authorizes either contact. Both remain serious pending board-local authority. The F.Cu report also names the wrong location (the pour crossing, not the trace). A proposed isolate narrowing was measured and reverted. Detail below |
| MNT Reform laptop (system) | all motherboard revisions | Standby battery drain: LPC + keyboard MCU stay powered through an always-on 3V3 rail with no hardware cutoff | community.mnt.re #559 | No (primary fix is firmware deep-sleep; the always-on rail is by design) | n/a | OUT OF SCOPE |
| Watchy | v3 | USB_DET (GPIO21) held HIGH ~500 ms after wake -> immediate re-wake loop | Watchy firmware #265 | No (firmware wake-source logic vs ESP32-S3 boot pin behaviour) | n/a | OUT OF SCOPE |
| Olimex ESP32-EVB | rev C -> C1, H1 -> I | ESP_EN / reset RC timing tuning (R38, C28, R24, C31) for start-up reliability | Olimex HARDWARE changelog | No (RC start-up timing margin, not a connectivity/rating defect) | n/a | OUT OF SCOPE |
Summary statistic
The count below is of faults documented in a revision history, which is what this page was built to calibrate against. Three rows in the matrix are a different thing and are deliberately not counted in it: the ODrive, MWGEN-G1 and emonTx copper rows are adjudications of findings hauksbee raised live, on boards nobody had filed a fault against, and their ground truth is a sibling layout, a second tool's DRC, or the board's own fabrication output rather than a changelog. They are in the matrix because the adjudication has to live somewhere the next reader finds it, and each carries its own test.
In-scope known faults validated: 8. Caught statically by hauksbee: 6;
executed by firmware co-sim: 1; remaining static miss: 1. The original I2C
pull-up gold row; the Olimex ESP32-EVB GPIO0 strapping fault caught by the
strap_pin lint; and the four power-IC faults the behavioural device-model
framework now carries (detailed below). The two "control input on a Hi-Z GPIO
with no pull" rows are not structurally separable from benign twins on a static
netlist (proven below) and so are honest static misses; they become decidable by
running the firmware (the boot-coverage assertion). Round 5 executed the
Watchy row that way: on the real Watchy v1.5 board under the Espressif QEMU
ESP32 backend, two-sided GREEN/RED (see the firmware-boot section below). The
ZSWatch DISPLAY-EN row remains a static miss because ZSWatch's nRF5340 has no
co-sim backend yet (docs/cosim/MCU.md).
No check was forced and no false positive was shipped to inflate the count: the
strap lint fires on exactly the Olimex GPIO0 clock and nothing else across the
whole famous corpus, the behavioural models add physics, not findings, on clean
boards (behavioral_parts_do_not_manufacture_faults_on_clean_board), and the
Watchy boot-coverage execution is two-sided (it goes RED on a firmware that does
not drive the net), so it is a measurement, not a vacuous pass.
Every catch is a true catch against ground truth, and every miss is annotated with exactly what hauksbee would need to model. A tool that claimed to catch the LTC4020 overdraw from the netlist alone would be lying; the model is what earns the catch.
The honest reach limit on the strap catch. hauksbee flags the GPIO0 fault on every Olimex ESP32-EVB revision in the corpus (rev B through L), not just the pre-fix ones. That is not a false positive: the rev-E fix is a time-domain power-sequencing change (FET3/FET4 gate PHY power via OSC_DIS so the PHY does not drive GPIO0 until the oscillator has stabilised), and it does not alter the GPIO0 net - the 50 MHz oscillator (CR1) still reaches GPIO0 through R36 (10R) in the rev-L netlist, byte-for-byte as in rev D (verified from the files). A static netlist check cannot see the fix, so it correctly reports the netlist-visible fault on all revisions and we say so plainly rather than forcing a "clean on the fixed rev" the netlist does not support. The clean two-sided discriminator (fires on a clock-strapped GPIO0, silent on a pull-up-strapped one) is proven on a constructed pair instead - see the strap-catch detail below.
The four behavioural-model catches:
- LTC4020 ILIMIT overdraw (mb2.5 -> mb3.0): the averaged buck-boost block reads
the board's R8 (ILIMIT) and R49 (one equal 10 mOhm sense-path leg) and applies
the LTC4020 transfer function. The same law is catalogued as a
protection_limit; static ampacity deliberately does not treat a converter threshold as continuous board load. In the regression harness the behavioural model predicts 95.4 W on mb2.5 (R8 = 100k) and 34.1 W on mb3.0 (R8 = 7.15k). The first exceeds a 60 W brick budget and the fix clears it. These are datasheet-derived model outputs, not claimed bench measurements; the independent maintainer evidence reports the faulty board drawing 75--88 W. - LTC4020 RNG/SS destabilisation (mb2.0 -> mb2.5): the converter FSM destabilises only when RNG/SS is genuinely driven low (mb2.0 wires it to the LPC GPIO); on mb2.5 the pin is NC, so it stays stable.
- LTC6803-4 absent-cell balancer leak (mb2.0 -> mb2.5): a leak law over the unused-cell tie network reads 0.28 A through R52 = 100 ohm on mb2.0 and ~0 A on mb2.5, where R52 is replaced by a blocking Schottky.
- nPM1300 SHPHLD -> VSYS (1.2.0 -> 1.2.1): the modelled internal pull drives the SHPHLD net to ~3.66 V (VSYS) in sleep, and the MCU GPIO (M601) shares that net on 1.2.0 (so it is fed VSYS) but was removed on 1.2.1.
The behavioural numbers are derived from the real board resistor values read off the corpus (R8, R49, R52) and the documented revision fixes, not asserted.
Gold row detail (FLAGGED)
ZSWatch DevKit: missing I2C pull-ups (1.2.0 -> 1.2.1)
Documented fault. Watch-DevKit-HW CHANGELOG 1.2.1, "Added":
Add missing pull-ups for I2C lines (#158)
Faulty 1.2.0, at the s-expression level (net_probe). The RTC sits on the
secondary side of the PCA9306 I2C level translator (IC506), and that side has
no resistor to any rail. (The net names below read SDA_2 while the pad
func beside them still reads SDA_{2}: net names are unescaped to the
spelling the schematic shows, pad function strings are quoted from the file
verbatim.)
NET Net-(IC506-SDA_2) (id 145)
IC506 val='PCA9306JK' pad=5 func='SDA_{2}' (level translator, secondary side)
IC507 val='RV-8263-C8' pad=1 func='SDA' (RTC)
NET Net-(IC506-SCL_2) (id 146)
IC506 val='PCA9306JK' pad=6 func='SCL_{2}'
IC507 val='RV-8263-C8' pad=8 func='SCL'
The PCA9306 is a bare pass-gate with no integrated pull-ups (hauksbee
deliberately does not exempt it; only auto-direction translators like the
NTS0104 carry their own pulls), so this translated bus genuinely needs its own
resistors. hauksbee run ... --lint on 1.2.0:
net-lint: 4 finding(s)
[low] missing_i2c_pullup - I2C SDA net '.../MCU/SDA' ... bus breaks out to a header ...
[low] missing_i2c_pullup - I2C SCL net '.../MCU/SCL' ... bus breaks out to a header ...
[medium] missing_i2c_pullup - I2C SDA net 'Net-(IC506-SDA_2)' ... on-board master and peripheral, no pull-up present
[medium] missing_i2c_pullup - I2C SCL net 'Net-(IC506-SCL_2)' ... on-board master and peripheral, no pull-up present
The two medium findings are exactly the missing RTC pull-ups. (The two Low findings are the primary-side bus breaking out to a 0.1" header, the intended "pulls on the attached module" convention from the famous sweep, correctly Low.)
Fixed 1.2.1, same nets:
NET Net-(IC506-SDA_2) (id 145)
R505 val='3k3' pad=1 (added)
IC506 ... IC507 ...
NET Net-(IC506-SCL_2) (id 146)
R504 val='3k3' pad=1 (added)
IC506 ... IC507 ...
hauksbee run ... --lint on 1.2.1: net-lint: no findings. The two 3k3 pull-ups
R504/R505 make the lint clean. hauksbee flags exactly what the next revision
fixed and stops flagging once it is fixed.
Why this discriminator is real, not a parser quirk. The shipped ZSWatch
mainboard (a corpus board) uses the same PCA9306 + RV-8263 topology with the
pull-ups present, and hauksbee is clean on it. This is the exact circuit the
famous sweep's deep-dive C2 analysed (where the pulls were present and an
earlier tool bug had hidden them). The faulty DevKit revision is where the pulls
were genuinely absent, and the lint, with its 0201-extra-pad and local-rail fixes
from that sweep, now reads it correctly. Cross-checked in
zswatch_mainboard_rtc_i2c_is_clean.
External-pool adjudication detail (2026-08-09)
The external-pool iteration qc/evidence/runs/2026-08-09-external-01.json drew
five never-before-used boards and two came back with serious shorts: six on the
MWGEN-G1 and two on the emonTx V3.4.5. Both are public designs by people who know
what they are doing, which is enough to put a short report under suspicion of
being our defect until it is chased to the file (the project meta-lesson in
BUG_HUNT.md). Chasing them separated cleanly: one set is real
copper the designer did not intend, and the other was our reader treating a pour
outline as a fill.
One thing this page does not claim about either board is production status. The emonTx V3.4.5 is a released revision of a product line whose six preceding revisions sit beside it in the same tree (V3.2.x and V3.4.0 through V3.4.4); the MWGEN-G1's status is not established here, and neither verdict depends on it. Both are about what is in the design files.
MWGEN-G1: six pad overlaps, and KiCad agrees on all six
The suspicion going in was a deliberate structure: a net tie, a pad-in-pad RF
launch, a capacitive stub drawn as a zone. It is none of those. There is no
net_tie attribute and no NetTie footprint anywhere in the file, every finding is
a pad against a pad with no zone or track involved, and the geometry is a
placement collision around the reference-input corner. D503 is the cause of five
of the six:
(footprint "Diode_SMD_AKL:D_SMA_TVS" (layer "F.Cu")
(at 143.3445 90.6395)
(attr smd)
(fp_text value "SMAJ48CA" ...)
(pad "1" smd rect (at -2 0) (size 2.5 1.8) (layers "F.Cu" "F.Paste" "F.Mask")
(net 125 "/Clock reference/Input Clocks/REFIN_GND") (pinfunction "K") ...)
(pad "2" smd rect (at 2 0) (size 2.5 1.8) (layers "F.Cu" "F.Paste" "F.Mask")
(net 1 "GND") (pinfunction "A") ...)
A TVS in an SMA body has 2.5 x 1.8 mm pads, so pad 1 spans x 140.095..142.595, y 89.740..91.540. Everything around it is smaller and closer:
- D203 (SOT-23, rotated 180) puts pad 1
/Clock reference/REFOUT_SECat (143.648, 90.449), size 1.475 x 0.6, spanning x 142.9100..144.3850. D503 pad 2 (GND) starts at x 144.0945. They overlap by 0.2905 mm in x, and hauksbee reports -0.150 mm. - D203 pad 3 and D202 pad 3, both
Net-(D202-Pad3), sit at y 89.199..89.799, and D503 pad 1 starts at y 89.740. A 0.060 mm overlap, twice. - R204 pad 2 and C205 pad 2, both
/Clock reference/Input Clocks/REFIN(0603 hand-solder pads, y 91.524..92.474), meet D503 pad 1's top edge at y 91.540. A 0.015 mm overlap, twice. - Separately, the shield can J206 (
RF_Shielding:Laird_Technologies_BMI-S-205-F) has a GND fence pad(at 18.95 0 90) (size 1 3.3)in a footprint at(at 162.55 98.05 90). KiCad writes a pad's angle as its ABSOLUTE board-frame orientation, so that 1 x 3.3 box lies along x: it spans x 160.90..164.20, y 78.60..79.60. FB204 pad 1Net-(FB204-Pad1)spans x 161.15..162.20, y 78.625..79.575, which is entirely inside it. Exporting gerbers from this file withkicad-cliagrees, flashing apertureR,3.300000X1.000000at (162.55, 79.1); the upstream ships no gerbers for this board, so that is a generated check rather than the shipped-fabrication kind the emonTx section uses. This one is not an edge graze but a pad swallowed whole.
What the reported gap numbers are, and are not. Four of the six are the exact overlap: -0.059614 twice against a true 0.0596 mm (89.7991 - 89.7395), and -0.015386 twice against a true 0.0154 mm (91.5395 - 91.5241). The other two are floors, for two different reasons, and each is far from tight.
Where inset pad polygons interpenetrate, the depth saturates: a roundrect is carried as an inset polygon plus a corner radius, so once the inset polygons cross the reported figure stops at the summed radii. D503/D203's -0.150 mm is that: pad 2 of D503 is a plain rect (radius 0) and D203's pad 1 a roundrect (radius 0.15), summing to the 0.15 reported against a real 0.2905 mm x-overlap.
Where one pad is wholly inside another, a different branch takes over. Containment
returns edge.min(0.0) - 1e-6 (drc.rs, the polygon-polygon arm), so a fully
engulfed pad reports -0.000001 mm: FB204 inside J206's fence pad measures
+0.025 mm of inset-polygon edge distance, the clamp takes it to zero, and the
epsilon is all that is left. The most complete overlap on the board therefore
carries the smallest number in the table.
So a magnitude here is a lower bound on the overlap, exact in four cases and
badly under in two. The sign is what the short verdict rests on, and it is right
in all six; the magnitudes are pinned in the test as a regression guard, and are
no part of the agreement with KiCad, whose report carries no gap at all for a
shorting_items violation.
Ground truth, in the same shape as the ODrive row's: the design's own rules
forbid it. MWGEN-G1.kicad_pro sets shorting_items and clearance to error
and its drc_exclusions list is empty. Those two severities are KiCad's defaults
rather than a deliberate tightening, which is the same strength of claim the
ODrive row rests on: the design did not loosen the rule that forbids this, and no
exclusion records an instance of it being looked at and accepted. That is weaker
than "nobody looked", and it is all the file supports. The test reads the
.kicad_pro and asserts all three values, so the claim cannot rot.
And KiCad 9.0.3 run over the same file (kicad-cli pcb drc --format json) reports
six shorting_items violations, which are the same six net pairs anchored at the
same pad centres:
shorting_items | nets /Clock reference/Input Clocks/REFIN_GND and .../REFIN | (141.3445, 90.6395) (139.86, 91.999114)
shorting_items | nets /Clock reference/Input Clocks/REFIN_GND and .../REFIN | (141.3445, 90.6395) (141.45, 91.999114)
shorting_items | nets /Clock reference/Input Clocks/REFIN_GND and Net-(D202-Pad3) | (141.3445, 90.6395) (141.7725, 89.499114)
shorting_items | nets /Clock reference/Input Clocks/REFIN_GND and Net-(D202-Pad3) | (141.3445, 90.6395) (139.7975, 89.499114)
shorting_items | nets GND and /Clock reference/REFOUT_SEC | (145.3445, 90.6395) (143.6475, 90.449114)
shorting_items | nets GND and Net-(FB204-Pad1) | (162.55, 79.1) (161.675, 79.1)
That report is committed, not paraphrased:
crates/hauksbee-extract/tests/mwgen_g1_2fc77c90_kicad_9_0_3_shorts.json holds
the six violations verbatim (item descriptions, pad UUIDs, positions), and
mwgen_g1_2fc77c90_oracle.md beside it records the command, the input hash and
what was trimmed. The gold-row test derives its expected set from that file, so
the cross-check runs in CI without CI needing KiCad. That pins hauksbee against
the recording; it does not re-run KiCad, so a change in KiCad's own reading stays
invisible until someone re-runs the recorded command.
KiCad also reports 32 courtyards_overlap and 16 solder_mask_bridge violations
board-wide, of which 6 and 6 fall inside the two boxes these shorts sit in
(x 140-145 / y 88-93, and x 160-164 / y 77-81). So the crowding is not confined to
this corner, but it is concentrated here: the same story from two other
directions. What that establishes is that the shipped file is not
DRC-green. It does not establish that nobody ran the DRC, or that nobody looked at
these and accepted them: a designer can do both without recording an exclusion, and
an empty exclusion list only says none was recorded. Six REAL DEFECTS, and a gold
row rather than a corpus silence entry.
emonTx V3.4.5: a declared ground tie, reported as a short
The emonTx reported one GND <-> AGND short per copper layer, at the identical coordinate, gap 0.000 mm. That pattern is what a deliberate single-point star ground looks like, so the question was which it was. Both pours are same-rank and their outlines genuinely overlap:
GND layer 1 isolate 0.3048 ... (33.782, 80.772) (33.647, 43.205) (87.376, 43.205) ...
AGND layer 1 isolate 0.3048 (34.544, 78.359) (34.409, 42.856) (86.733, 42.856) (86.233, 78.359)
GND layer 16 isolate 0.3048 ... (33.782, 80.772) (33.647, 43.205) (87.376, 43.205) ...
AGND layer 16 isolate 0.00030625 (34.409, 42.856) (86.733, 42.983) (86.233, 78.232) (34.544, 78.232)
GND fills below y=43.205 and AGND above y=42.856, so the outlines overlap in a 0.349 mm band on both layers at the corner the finding sits in, and (34.41, 43.20) is where the two rings cross. Nothing in that geometry distinguishes the layers.
The oracle, and the mistake to avoid. This upstream ships its fabrication
output beside the design:
hardware/V3.4.5/GERBERS emonTx V3.4.5_2021-04-07/CAMOutputs/GerberFiles/. The
useful question to ask of it is not "how far apart are the pours" but does any
single filled region of the layer contain vias of both nets, which needs no
distance and no resolution choice. Answers, by exact point-in-polygon over the
dark G36 regions:
copper_bottom.gbr: yes, the 6378-vertex region. The pours abut along the seam; there is no isolation channel at (50, 43.2).copper_top.gbr: also yes. Dark region index 9, 770 vertices, containing the GND vias at (72.771, 38.862), (77.216, 42.672), (77.343, 41.021), (78.867, 41.021) and (78.867, 42.545) and the AGND via at (81.915, 47.498). Rasterising the layer with polarity, strokes and flashes finds the same single component (about 54 mm2, 11 GND items and 7 AGND items) at 50 and 100 px/mm, and an independent reviewer's pass reports it surviving erosion until the bridge is around this board's 0.2032 mm minimum trace, so it is routed copper, not a rounding artefact.
The top-layer pour FILLS are 8.236 mm apart, at (42.110, 70.967) to (33.874, 70.997), and that is true and irrelevant: the pours do not meet, and the nets are joined anyway, by routing near U1. An earlier pass of this adjudication measured the pours, concluded "no contact on F.Cu", and was wrong about the layer. Reading only the pours answers a question about pours.
The tie is declared, not inferred. emonTx V3.4.5.sch wires the AGND supply
symbol AGND7 (library supply1, deviceset AGND, no package) to the GND supply
symbol SUPPLY6 in one segment of net GND. That segment is absent from
V3.4.0's schematic and present in V3.4.1 through V3.4.5, the same boundary at
which three of the four ground pours were raised from 0.00030625 to 0.3048 mm.
A designer drew the tie, in the revision where they started isolating the pours,
and kept it for four more releases.
Verdict: both reports are about real copper, and the schematic declares the net pair. hauksbee is right that GND and AGND are in contact on both layers. It is wrong about where on F.Cu, naming the pour-outline crossing rather than the trace. The schematic establishes net-pair intent but cannot locate either board contact, so both observations remain SERIOUS until board-local authority identifies an intended physical join.
Context added by reading the schematic. That input is now a supported
companion. --schematic <FILE>, or a .sch sitting beside the board under the
board's own name, is read for Eagle's supply-symbol tie construct
(hauksbee-extract/src/eagle_sch.rs). A matching contact keeps its layer,
location, measured gap, SERIOUS severity and --strict gate while naming
AGND7 wired to SUPPLY6 in net GND as context. Run against the fabrication-era
files, the parser finds exactly one declared
tie in the whole of V3.4.5's schematic, and it covers both layers' contacts; it
finds none in V3.4.0, independently reproducing the revision boundary recorded
above. With no schematic the finding stays SERIOUS and names the schematic as the
contextual upload, and a schematic that declares nothing adds nothing, so
V3.4.0's reported contacts stay serious even with its own schematic supplied. Both
directions remain opt-in external-corpus adjudications
(emontx_ground_tie_is_real_copper_on_both_layers_and_both_are_reported and
emontx_v340_schematic_declares_no_tie_so_its_shorts_stay_serious in
crates/hauksbee-extract/tests/known_faults.rs); explicit invocation fails when
the corpus files are absent. The always-run release contract instead uses the
tracked declared and undeclared pairs under
crates/hauksbee-extract/tests/fixtures/eagle_ties/. The two pour over-reports
below are untouched: they are a fill-reconstruction problem, not a declaration
problem.
Historical exploratory sweep over the available Eagle pairs. The
first cut admitted any symbol carrying a direction="sup" pin, which is wrong
because ordinary Eagle libraries mark a real component's power pins that way. Run
over the twelve .brd/.sch pairs on hand it invented declarations on two
unrelated designs: 6 on margay_logger/Hardware/Margay.sch (the SD-MMC symbol,
13 pins, 4 of them sup, so an SD socket "declared" ground tied to MISO,
3V3_AUX, SCK_BUF and three more) and 19 on emonTx V3.2.sch (the XBEE
symbol, 20 pins, 2 sup), including 3.3V to GND. Any of those would have
attached false intent context to a real rail-to-ground short. Requiring a
single sup pin, one gate, and no packaged device behind the deviceset takes both
boards to zero while leaving the emonTx tie intact. That twelve-pair exploratory
sweep was not retained as a release artifact, so it is historical diagnosis, not
current completion evidence. The release contract is the always-present declared
and undeclared pair under crates/hauksbee-extract/tests/fixtures/eagle_ties/,
plus focused structural, packaged-part, multi-pin and library-URN collision tests.
Reproduce with cargo run -p hauksbee-extract --example sch_ties -- <brd> <sch>.
The multi-pin case is pinned by a_multi_pin_component_with_supply_pins_declares_nothing
and the packaged case by a_packaged_deviceset_is_never_a_supply_symbol, both in
crates/hauksbee-extract/src/eagle_sch.rs.
The isolate narrowing: tried, measured, reverted
Before the F.Cu contact was found, the F.Cu report looked like a false positive,
and the fix looked like keying the pour-to-pour rule on isolate (report the
overlap only when a pour asks for no gap). That change was implemented, measured
and then reverted. The measurement is worth keeping, because it is what the next
attempt has to beat.
Registering each revision's shipped export against its own drill file (V3.4.0 at (1.2195, 19.6690) mm, worst via residual 0.8 um over 108 vias; V3.4.2 at (1.1430, 19.6690), 0.9 um over 123), then labelling connected copper and asking whether any component holds vias of both nets:
| Revision, layer | pour isolates | contact? | outline rule | isolate rule |
|---|---|---|---|---|
| V3.4.0 F.Cu | both 0.00030625 | no | flags: wrong | flags: wrong |
| V3.4.0 B.Cu | both 0.00030625 | yes | flags: right | flags: right |
| V3.4.2 F.Cu | both 0.3048 | no | flags: wrong | silent: right |
| V3.4.2 B.Cu | GND 0.3048, AGND 0.00030625 | yes | flags: right | flags: right |
| V3.4.5 F.Cu | both 0.3048 | yes (the trace above) | flags: right | silent: wrong |
| V3.4.5 B.Cu | GND 0.3048, AGND 0.00030625 | yes | flags: right | flags: right |
Four of six either way. The narrowing does not improve accuracy; it trades the
V3.4.2 F.Cu false positive for a false negative on V3.4.5's F.Cu, and this
detector's own note on SHORT_TOUCH_EPS_MM says which side of that trade is
worse: "Under-reporting a short is the worst failure this detector has: it is the
one finding a board cannot ship with." So the guard came out and the rule is as it
was, with its error rate now measured rather than assumed.
Two things the exercise established that survive the revert. A near-zero
isolate is necessary but not sufficient for two pours to merge: V3.4.0 sets
both top pours to 0.00030625 and their fills stay apart, so no cut through
isolate can decide contact, and the 1 um threshold that was chosen inside the
measured band was answering a question the band cannot settle. And the outline
rule over-reports twice in six, which is now written down in
../about/LIMITATIONS.md with the reason (the fill is
never reconstructed) rather than left as a silence.
(V3.4.1 through V3.4.4 are not pinned in corpus.toml, which lands only V3.4.5
and V3.4.0, so the two V3.4.2 rows need a full upstream checkout to re-derive.
V3.4.1's archive is a V3.2.1 export; V3.4.3 and V3.4.4 ship gerbers that were not
measured.)
Strap-pin catch detail (FLAGGED)
Olimex ESP32-EVB: 50 MHz PHY clock on the GPIO0 strapping pin
Documented fault. GPIO0 is an ESP32 boot strapping pin that must be a stable
HIGH at the reset latch window for normal SPI boot; a LOW (or unstable) level
sends the chip into download mode. The Olimex ESP32-EVB wires the LAN8710A
Ethernet PHY's XTAL1/CLKIN (REF_CLK) to GPIO0, and a free-running 50 MHz
oscillator (CR1) drives that net. Espressif states it plainly (ESP-IDF Ethernet
API): "GPIO0 is also an important strapping GPIO on ESP32. If GPIO0 samples a
low level during power-up, ESP32 will go into download mode [...] This could fail
for some PHY device (i.e., it still outputs signal to GPIO0 even in reset
state)." Olimex's rev-E changelog adds "L5, FET3, FET4, C29, R50, R51 [...] to
enable powering on the PHY after the 50MHz oscillator has been started."
At the s-expression level (net_probe, rev D PCB; identical in rev L). Net
/GPIO0/XTAL1/CLKIN carries:
U3 pad 25 ESP-WROOM-32 GPIO0 (the strapping pin)
U4 pad 5 LAN8710A XTAL1/CLKIN (PHY REF_CLK)
R36 pad 2 10R -> R36 pad 1 = Net-(CR1-OUT)
R54 pad 1 NA/R0603 -> +3V3 (the would-be GPIO0 pull-up, DNP - not fitted)
CR1 Q50MHz osc pad 3 = OUT -> R36 -> GPIO0 ; pad 4 = +3V3 ; pad 1 = OSC_DIS
The 50 MHz oscillator CR1 reaches GPIO0 through the 10R series R36, and the
only would-be bias (R54, a pull-up to +3V3) is marked DNP. So the strap pin
sees a free-running clock at the reset latch, with no static pull to hold it
HIGH: exactly the documented failure.
The new check. A per-part strap table now lives in the model db
([[models.straps]] in crates/hauksbee-models/db/mcu.toml, cited per entry to
the Espressif/ST/RP datasheets), and an engine-layer lint
(crates/hauksbee-engine/src/checks/straps.rs, run by hauksbee run <board> --lint) examines the net each strap pin sits on. It flags (High) a strap net
that a free-running clock source reaches with no override:
[high] strap_pin - U3 strap pin gpio0 (gpio0) net '/GPIO0/XTAL1/CLKIN' carries a
free-running clock source CR1 (Q50MHz/...): a clock present at the reset latch
can mis-strap the part (GPIO0 high = SPI boot (normal); low = download/UART boot.
A 50 MHz clock here randomly enters download mode (Olimex ESP32-EVB))
Calibration (zero false positives, the bar for shipping). Swept across the
whole famous corpus, the strap lint fires on exactly the Olimex GPIO0 clock
and nothing else. Every other strap on every board is correctly biased and stays
silent, and the clean is real (not vacuous) because the parts genuinely
resolve and the straps are genuinely examined: Watchy's ESP32 GPIO0 (held by the
internal pull-up, no clock), every other Olimex strap (GPIO2 pulled high via the
2.2k RM1, GPIO15 pulled high via R29 10k, GPIO12 pulled low), the RP2040
QSPI_SS/BOOTSEL on rp2040-minimal and SparkFun Thing Plus (flash-CS idle-high),
and the nRF/AVR boards that have no ESP32 straps at all. The one false fire the
calibration did surface (GPIO15 appearing to "reach" CR1 because its pull-up
and the oscillator's VDD share the +3V3 rail) was a real bug in the check's
reachability, chased to ground and fixed (the hop now never travels through a
rail/ground), in the project meta-lesson pattern. Pinned by
crates/hauksbee-engine/tests/strap_lint_corpus.rs (5 corpus-gated tests,
including a corpus-wide guard that the lint fires only on the Olimex GPIO0) and
checks::straps unit tests (the synthetic two-sided discriminator: a constructed
ESP32 board fires when a 50 MHz oscillator reaches GPIO0, goes clean when GPIO0
carries a pull-up instead).
The honest reach limit. As noted in the summary, the lint fires on all Olimex revisions, because the rev-E fix is a time-domain power-sequencing change not visible in the GPIO0 net. The catch is of the netlist-visible fault (a free-running clock source reachable to a strap pin with no override); the time-domain question of when the clock appears relative to the latch window is out of a static netlist check's reach, and the clean two-sided proof is the constructed synthetic pair rather than rev D vs rev L.
Two further reach limits of the wrong-bias arm are recorded so the bar stays
honest as the corpus grows. (i) A momentary boot button to a net named GND is
indistinguishable, on a static netlist, from a permanent pull-down: a real
BOOTSEL/boot-button design routes the button's series resistor to a button node,
not directly to GND, so the corpus is clean, but that is incidental to net
naming, not a structural guarantee. (ii) A boot-select strap carrying both a
correct and a wrong-direction pull (a divider) is netted out and not flagged (the
split-divider guard, tested by boot0_with_an_opposing_correct_pull_does_not_fire).
Firmware-boot control-net verification (the Hi-Z control-input class)
The two "control input on a Hi-Z GPIO with no pull" misses below (Watchy RES#,
ZSWatch DISPLAY-EN) are not structurally separable on a static netlist - that
finding stands. But they become decidable by running the firmware: a new
hauksbee-ci assertion kind, boot-coverage, runs the co-sim from reset and
requires the MCU to drive the control net to a defined level within a deadline,
with no fault during the boot window before it. This turns "we cannot know the
intended default" into "watch what the firmware actually does". It is proven
two-sided on a constructed AVR board whose MOSFET gate floats at reset (variant-A
firmware drives it -> PASS; variant-B never does -> FAIL, naming the net;
crates/hauksbee-ci/tests/boot_coverage.rs, documented in docs/ci/CI.md).
Round 5 executed the Watchy row on its own board. The ESP32-PICO-D4 model now
maps to the qemu:esp32 backend (the same proven Espressif QEMU path the ESP32
demo uses), so the unmodified Watchy v1.5 board file co-simulates. boot-coverage
on the real RES net (GPIO9, no pull in v1.5) is two-sided: a reduced
display-init firmware that drives GPIO9 HIGH passes (RES reaches 2.6 V with the
boot window clean), and a firmware that drives other GPIOs but never GPIO9 fails
(RES observed across [0.000, 0.000] V and named).
The instant it crosses 2.6 V is not a reproducible number, and quoting one as
if it were would misrepresent the measurement. QEMU's timing varies run to run:
four consecutive runs put the crossing at 336, 272, 280 and 280 ms. What the
assertion checks, and what is stable, is the margin: the crossing lands inside
the spec's 700 ms deadline_ms every time, with roughly half the window to
spare. Read the result as "comfortably in time", not as a timestamp. The firmware is a reduced Watchy display-init stand-in
(it reproduces only the e-paper reset behaviour issue #14 is about and maintains
the GPIO mailbox the QEMU backend needs; the full GxEPD2 stack would not mirror
GPIO, see docs/cosim/MCU.md), so the verdict is labelled a reduced firmware-level
validation, not a claim about the full Watchy firmware's exact timing. Pinned by
watchy_v15_display_res_driven_passes / _undriven_fails
(crates/hauksbee-ci/tests/boot_coverage.rs, corpus + QEMU gated). Full
derivation in docs/evidence/FAMOUS_SWEEP.md Round 5.
The ZSWatch DISPLAY-EN row is still not flipped: ZSWatch's nRF5340 has no
co-sim backend yet (see docs/cosim/MCU.md), so the mechanism is recorded as
ready-for-backends, not applied to it.
Documented-miss detail (MISSED, not structurally separable)
The "control input on a Hi-Z GPIO with no pull" class
Two independently-cited faults share one structure:
Watchy #14 (v1.5 -> v2.0): the e-paper RES# pin is on a normal GPIO with no pull-up. The reporter:
RES# of the display is supposed to be left connected to HIGH. If it is not left in that state the Display hardware will not fully enter deep sleep. [...] the RES pin is connected to a normal GPIO and not to a RTC GPIO. GPIOs are left high impedance in deep sleep and therefore not driven to any value.
Fix (v2.0): add
R20(100K) to 3V3. Confirmed at the netlist level: in v1.5 theRESnet is{J1.pad10 (display), U1.pad28 (ESP32 GPIO)}; in v2.0 it additionally carriesR20=100K.ZSWatch DevKit #123 (1.1.0 -> 1.2.0): the
DISPLAY-ENnet (the OE pin of the NTS0104 level translator,IC304/IC604) is driven only by GPIO P0.20, no pull. Fix (1.2.0): addR613/R614(100K). Confirmed: in 1.1.0DISPLAY-EN = {M301.A5=P0.20, IC304.pad12=OE}; in 1.2.0 it additionally carriesR613/R614.
hauksbee's existing floating_control_pin check requires the control net to have
degree 1 (only the control pin, nothing else). Both faults have degree 2 (the
control pin plus a driving GPIO), so the check is silent. That is correct,
because the net is not floating in the strict sense; it is driven, just by a pin
that goes Hi-Z at power-up / sleep.
Could a structural check catch it? A prospective check was specified ("a
dedicated EN/OE/RST/CS pin whose only driver is an MCU GPIO, with no pull
resistor to a rail or ground"). Rather than commit it on a hunch, it was
implemented as a probe (kept, not thrown away, as
crates/hauksbee-extract/examples/ctrl_input_probe.rs) and measured against the
faulty board and the clean corpus before any change to net_lint(). On the
faulty DevKit 1.1.0 (Dev-Kit.kicad_pcb) the probe fires on five nets:
DISPLAY-EN ctrl=[IC304:output-enable] drivers=[M301.A5=P0.20] <- the documented fault
DISPLAY-CS ctrl=[X307:chip-select] drivers=[M301.B3=...] <- normal SPI CS, NOT a fault
DISPLAY-RST ctrl=[X307:reset] drivers=[M301.B5=...] <- normal display reset, NOT a fault
VIB-EN ctrl=[IC206:enable] drivers=[M301.E9=P1.05] <- haptic driver EN, NOT a fault
RESETn ctrl=[X310/M301:reset] <- MCU reset button net
Only DISPLAY-EN was fixed by the project. Five clean, shipped corpus
boards were swept, and three of them produced 6 false positives between
them, every one a correct design (a GPIO-sequenced power-gate, an idle-at-boot
SPI CS, an off-at-boot haptic EN). Enumerated so the claim is auditable, not
asserted:
| Board (clean, shipped) | Net the probe wrongly flags | Why it is correct |
|---|---|---|
| ZSWatch mainboard | RESETn (M601 reset) |
MCU reset, driven by the SWD/USB SBU lines |
| ZSWatch mainboard | DISPLAY-CS (X608 CS) |
SPI chip-select, idle-high until firmware drives it |
| ZSWatch mainboard | DISPLAY-RST (X608 reset) |
display reset driven by firmware; brief Hi-Z is harmless |
| ZSWatch mainboard | VIB-EN (IC505 enable) |
haptic driver; off-at-boot is the desired default |
| MNT Reform mb 3.0 | PCIE1_PWR_EN (U20 enable) |
intentional GPIO-sequenced PCIe power gate |
| Watchy v3 | Net-(D1-K) (U2 enable) |
boost-converter EN in its own startup network |
The ZSWatch mainboard is the killer: it is structurally identical to the DevKit (same DISPLAY-CS / DISPLAY-RST / VIB-EN topology) but the project never reported any of those as faults. The thing that makes DISPLAY-EN a fault and VIB-EN not a fault is the desired default state of the controlled load (a display that must be on by default vs a vibration motor that must be off by default), which is a behavioural judgment the netlist does not encode. The structures are byte-identical. A check firing here would be precisely the confident false-positive the project meta-lesson forbids, on the very same shipped board. So this is recorded as an honest miss, with the measurement as the evidence that forcing the check would do more harm than good.
The test fixed_control_pull_revisions_are_lint_clean pins that the fixed
revisions (Watchy v2.0, DevKit 1.2.0), which carry the pull, raise no
floating-control-pin finding, so any future attempt to add such a check must at
least stay silent on the corrected designs.
Behavioural-catch detail (FLAGGED via the device-model framework)
These four in-scope faults need a behavioural model of a specific power IC. That
model now exists: a declarative behavioural layer (named pins with internal
pulls / open-drain / enable semantics; finite state machines; averaged
buck/boost/buck-boost converter blocks with programmable input/output current
limits; and evalexpr-based current/voltage laws) defined in TOML alongside the
DB (crates/hauksbee-models/db/power_ics.toml), stamped and iterated each solver
chunk by crates/hauksbee-engine/src/behavioral.rs the same way the configurable
power supplies are. The model reads board-programmable resistors off the actual
netlist, so the faulty and fixed revisions diverge with no model edit.
MNT Reform LTC4020 ILIMIT overdraw (mb2.5 -> mb3.0). The handbook: "Fixed/ limited LTC4020 charge current overdraw (resistor R8 replaced with 7.15k)." The behavioural model is an averaged buck-boost converter whose input-current limit is programmed by R8 (ILIMIT) against the 0.01 ohm input sense path. The dynamic model reads R8 and R49. The static model records the same equation as a
protection_limit, so ampacity cannot mistake the threshold for operating current or charge it to the power rails/Kelvin stubs. The datasheet statesVILIMIT = 50 uA * RILIMITover an effective 0--1 V range, scaling a 50 mV full-scale current-sense threshold. Thus the nominal model with R8 = 100k (mb2.5) caps at 5 A and predicts 95.4 W in the harness; R8 = 7.15k (mb3.0) programs 1.7875 A and predicts 34.1 W. With a 60 W brick budget configured, the overpower fault fires on mb2.5 and clears on mb3.0. This validates the documented causal distinction, not exact agreement with the separately reported 75--88 W bench draw. Test:ltc4020_overdraws_on_mb25_and_is_clean_on_mb30.MNT Reform LTC4020 RNG/SS dual-role (mb2.0 -> mb2.5). Modelled as the converter's FSM: a
stable -> destabilisedtransition guarded by RNG/SS being pulled low. The pin is bound only when it sits on a real (non-unconnected-*) net, so the FSM can destabilise only when RNG/SS is genuinely driven, which mb2.0 does (the LPC CHG_RNG GPIO) and mb2.5 does not (NC). Test:ltc4020_rng_ss_destabilises_only_when_driven.LTC6803-4 balancer leak with an absent cell (mb2.0 -> mb2.5). Modelled as an
evalexprcurrent lawv_vplus / tie_ohmsfrom the pack top to the stack bottom, wheretie_ohmsis read off the board's R52 (the unused-cell tie). On mb2.0 R52 = 100 ohm gives 0.28 A of continuous leak from the ~28 V pack; on mb2.5 R52 is absent (replaced by a blocking Schottky D30), so the binder substitutes an open resistance and the leak is ~3e-11 A. Test:ltc6803_leaks_on_mb20_and_is_clean_on_mb25.ZSWatch nPM1300 SHPHLD -> VSYS -> MCU GPIO (#155). The SHPHLD pin carries an internal pull-up to VSYS, encoded as a behavioural pin pull. hauksbee now drives the SHPHLD net to ~3.66 V (VSYS) in sleep instead of seeing it float; on 1.2.0 the MCU GPIO (M601 = nRF5340 P1.06) shares that net and is thereby fed VSYS, while on 1.2.1 the GPIO was removed from the net. Test:
npm1300_shphld_feeds_gpio_on_120_and_is_clean_on_121.
Out-of-reach detail (now empty)
Earlier passes of this benchmark listed five in-scope faults as out of reach: the Olimex GPIO0 strapping fault (needed strap tables and clock-at-reset awareness) and the four power-IC faults (needed behavioural models of the LTC4020, LTC6803-4 and nPM1300). All five are now caught: the strap-pin lint covers the first (with its reach limit stated above), and the behavioural device-model framework covers the other four (detail above). The only remaining in-scope misses are the two not-structurally-separable control-pin rows, whose firmware-decidable path is described in the matrix.
Out-of-scope detail
- MNT Reform standby battery drain. The headline behaviour, but the root cause is architectural (the LPC and keyboard MCU are powered from an always-on 3V3 rail by design) and the primary fix was firmware deep-sleep (community.mnt.re #559). The later protected-battery-pack board adds a hardware undervoltage cutoff, but that prevents cell damage, it does not remove the drain. Not a connectivity or rating defect in a single revision; out of scope.
- Watchy v3 USB_DET re-wake loop (#265). An interaction between ESP32-S3 boot pin behaviour and firmware wake-source configuration. Firmware-domain; out of scope.
- Olimex ESP_EN / reset RC tuning (R38/C28/R24/C31 across revs). Iterative start-up timing-margin tuning, not a connectivity or rating defect. Out of scope (hauksbee would need a calibrated reset-threshold-vs-RC model and the brown-out detector's exact timing to adjudicate a margin).
Check improvements committed
A new boot-time check shipped: the strap-pin lint (strap_pin). It carries
per-part boot strapping tables in the model db (ESP32 classic / -S2 / -S3 / -C3,
STM32 BOOT0, RP2040 QSPI_SS/BOOTSEL, each cited to the part's TRM; AVR documented
as having no strapping pin), and flags a strap net a free-running clock reaches
with no override, or a strap pulled to the wrong level. It was calibrated to zero
false positives on the whole famous corpus (it fires on exactly the Olimex GPIO0
clock) before being trusted, in the same discipline as the famous sweep. The one
false fire the calibration surfaced (the GPIO15 rail-shared-with-VDD reachability
bug) was fixed at the cause. A new hauksbee-ci assertion kind, boot-coverage,
makes the Hi-Z-control-input class decidable by running the firmware (see the
detail section above and docs/ci/CI.md).
No check was added or changed by the 2026-08-09 external-pool adjudication, and
the attempt is worth recording because it was reverted on evidence. The Eagle
pour-to-pour arbitration reads any same-rank outline overlap between two signals
as a short. The emonTx V3.4.5 looked like a counterexample, and a narrowing that
required a near-zero isolate was implemented; scoring it against the six emonTx
layer-instances that ship fabrication data showed it right about four, the same as
the rule it replaced, having traded one over-report for a miss on a layer where a
trace genuinely joins the nets. The guard came out. What survives is the scoring
table in the detail section above, the two over-reports written down in
../about/LIMITATIONS.md, and a named unlocking input
for each (Eagle's fill for the over-reports, the schematic's net segments for the
net-tie blindness).
The blast radius of the reverted attempt was measured while it existed: it could
only change a verdict where two same-rank pours of different signals have
overlapping outlines, and a scan of every .brd in board-corpus finds zero
such pairs (the Eagle 5 binaries hauksbee refuses are not readable either way,
which is what the eurorack_binary_eagle entry is for). The only instances in
either tree are the twelve in the emonTx revision family. eagle_drc_corpus,
drc_corpus and the 52-test eagle_drc suite are green.
The earlier campaign forced no check. The decisive engineering call it produced
was a negative: the prospective "control input on a Hi-Z GPIO" check was
specified, measured against the clean corpus (via the committed
crates/hauksbee-extract/examples/ctrl_input_probe.rs, so the measurement is re-runnable), shown to
manufacture 6 false positives on three of the five swept shipped boards, and
rejected. The famous-sweep
lint already had the supporting
fixes (0201 extra-pad resistor counting, local-rail recognition, pass-gate
non-exemption) that let it read the ZSWatch gold row correctly, so no new code
was needed to catch the one catchable I2C fault. The false-positive discipline of
the sweep is therefore intact: cargo test --workspace is green, including the
netlint, drc_corpus, known_faults, strap_lint_corpus, and boot_coverage
suites.
Tests added
crates/hauksbee-engine/tests/behavioral_faults.rs (5 tests, corpus-gated the
same way: absent corpus skips, HAUKSBEE_REQUIRE_CORPUS=1 makes absence a hard
fail). Four are the two-sided power-IC gold rows above
(ltc4020_overdraws_on_mb25_and_is_clean_on_mb30,
ltc4020_rng_ss_destabilises_only_when_driven,
ltc6803_leaks_on_mb20_and_is_clean_on_mb25,
npm1300_shphld_feeds_gpio_on_120_and_is_clean_on_121); the fifth
(behavioral_parts_do_not_manufacture_faults_on_clean_board) is the
false-positive guard; the FIXED Reform mb3.0 binds both behavioural parts and
runs a normal solve raising zero behavioural faults. The framework itself is
unit-tested in crates/hauksbee-engine/tests/behavioral_framework.rs (8 tests:
pulls, FSM + open-drain, expression laws, averaged-converter regulation and
limits, and the custom-behaviour escape hatch).
crates/hauksbee-extract/tests/known_faults.rs (7 tests, corpus-gated:
absent corpus skips, but HAUKSBEE_REQUIRE_CORPUS=1 makes absence a hard fail so
the calibration cannot vacuously pass on a runner that should have the corpus):
zswatch_devkit_i2c_pullup_flagged_in_faulty_clean_in_fixed- the gold row: asserts >= 2 medium I2C findings on faulty 1.2.0 and zero I2C findings on fixed 1.2.1.zswatch_mainboard_rtc_i2c_is_clean- cross-check that the shipped mainboard (same RTC topology, pulls present) stays clean, so the contrast is a true discriminator.fixed_control_pull_revisions_are_lint_clean- a deliberately one-sided guard, NOT a catch:floating_control_pinfires on none of these boards (that is the whole point of the documented miss), so this only pins that the fixed Watchy v2.0 and DevKit 1.2.0 raise no such finding, i.e. if anyone ever adds a control-pin check it must at minimum stay silent on the corrected designs. Only the first test is a true faulty-vs-fixed discriminator.odrive_v2_attempt_ground_short_flagged_and_final_is_clean- the ODrive copper short gold row: the attempt layout carries exactly the one GND/AGND overlap and the final layout beside it is short-clean.mwgen_g1_pad_overlap_shorts_match_kicads_own_drc- the MWGEN-G1 gold row, pinned pair by pair (net pair, layer, and both overlapping footprints) against the committed recording of KiCad 9.0.3's own DRC on the same file, so losing one of the six or inventing a seventh both fail here. Also asserts the six gap magnitudes and the.kicad_proseverities and empty exclusion list.emontx_ground_tie_is_real_copper_on_both_layers_and_both_are_reported- the net-tie row. It reads the two shipped gerbers and asserts the oracle first (on each layer, at least one filledG36region contains vias of BOTH nets), then requires both reports to be present, then asserts the.schsegment that ties the AGND supply symbol to the GND one. So the test states the uncomfortable shape of this row: the findings are true about copper and the design declares the connection.emontx_v340_top_layer_pour_flag_is_a_known_false_positive- a test that pins a result which is partly WRONG, on purpose. V3.4.0 flags both layers; its bottom finding is real and its top one is the measured over-report. Encoded so that whoever implements fill reconstruction sees a red test rather than having to find this page, and knows which note to update.
crates/hauksbee-engine/tests/strap_lint_corpus.rs (5 tests, corpus-gated): the
strap catch. olimex_rev_d_gpio0_clock_flagged (the gold catch),
olimex_rev_l_still_fires_fix_is_not_netlist_visible (the honest reach limit,
pinned as ground truth), watchy_esp32_straps_are_clean and
rp2040_minimal_bootsel_is_clean (known-good, examined-and-clean), and
strap_lint_only_fires_on_olimex_gpio0_across_corpus (the corpus-wide zero-false-
positive guard). Plus checks::straps unit tests (no corpus): the synthetic
two-sided discriminator and the GPIO15 rail-share regression.
crates/hauksbee-ci/tests/boot_coverage.rs (4 tests): the boot-coverage two-sided
proof. gate_driven_promptly_passes (variant A drives the floating MOSFET gate)
and gate_left_floating_fails_naming_the_net (variant B never does), plus the
two Watchy arms on the real v1.5 board under the ESP32 QEMU backend,
watchy_v15_display_res_driven_passes and
watchy_v15_display_res_undriven_fails (corpus + QEMU gated). Plus a
spec-validation guard in spec_and_assertions.rs.
Reproduce
cd hauksbee
BIN=target/release/hauksbee; cargo build --release -p hauksbee-engine
C=../board-corpus/famous
# Gold row: flagged on faulty, clean on fixed.
$BIN run "$C/zswatch_devkit/v1.2.0/ZSWatch-Watch-DevKit.kicad_pcb" --lint # 2 medium
$BIN run "$C/zswatch_devkit/v1.2.1/ZSWatch-Watch-DevKit.kicad_pcb" --lint # no findings
# The documented misses (hauksbee is silent; the fix is a pull-up).
cargo run --release -q -p hauksbee-extract --example net_probe \
"$C/watchy_history/v1.5/Watchy.kicad_pcb" RES # no R on the RES net
cargo run --release -q -p hauksbee-extract --example net_probe \
"$C/watchy_history/v2.0/Watchy.kicad_pcb" RES # R20 present
# Why the control-input check was NOT added: the probe fires on the real fault
# AND on benign twins. 5 hits on the faulty board, 6 false hits on clean ones.
cargo run --release -q -p hauksbee-extract --example ctrl_input_probe \
"$C/zswatch_devkit/v1.1.0/Dev-Kit.kicad_pcb" # DISPLAY-EN + 4 benign
cargo run --release -q -p hauksbee-extract --example ctrl_input_probe \
"$C/zswatch_mainboard/watch/ZSWatch-Watch.kicad_pcb" # 4 false positives, shipped board
# Strap-pin catch: the Olimex GPIO0 50 MHz clock flags; the fix is not
# netlist-visible so it flags every rev. Known-good ESP32/RP2040 boards stay
# silent (the strap lint runs as part of `--lint`).
$BIN run "$C/olimex_esp32/HARDWARE/REV-D/ESP32-EVB_Rev_D.kicad_pcb" --lint # 1 high: GPIO0 clock
$BIN run "$C/olimex_esp32/HARDWARE/REV-L/ESP32-EVB_Rev_L.kicad_pcb" --lint # still 1 high (fix not netlist-visible)
$BIN run "$C/watchy_history/v2.0/Watchy.kicad_pcb" --lint # no strap_pin finding
# Boot-coverage two-sided demo (the Hi-Z control-input class, decided by firmware).
CI=target/release/hauksbee-ci; cargo build --release -p hauksbee-ci
$CI run crates/hauksbee-ci/examples/boot_gate_pass.toml # GREEN: gate driven within deadline
$CI run crates/hauksbee-ci/examples/boot_gate_fail.toml # RED: gate left floating, net named
# The two external-pool copper rows. These two boards sit at the corpus ROOT
# (the fetch layout), not under famous/. MWGEN-G1 flags six pad overlaps; the
# emonTx flags both layers, which its own gerbers say is right about the copper.
R=../board-corpus
$BIN run "$R/mwgen_g1/MWGEN-G1.kicad_pcb" --drc # 6 serious
$BIN run "$R/emontx3/hardware/V3.4.5/emonTx V3.4.5.brd" --drc # 2 shorts, one per layer
$BIN run "$R/emontx3_v340/hardware/V3.4.0/emonTx V3.4.brd" --drc # 2, and F.Cu is the over-report
# The MWGEN-G1 cross-check, if you have KiCad 9 installed: same six
# shorting_items, same net pairs, same pad anchors.
kicad-cli pcb drc --format json --output mwgen_drc.json \
--severity-error "$R/mwgen_g1/MWGEN-G1.kicad_pcb"
# CI calibration. HAUKSBEE_REQUIRE_CORPUS=1 makes corpus-absence a hard fail
# instead of a silent skip, so the gold row cannot vacuously green-out.
HAUKSBEE_REQUIRE_CORPUS=1 cargo test -p hauksbee-extract --test known_faults
HAUKSBEE_REQUIRE_CORPUS=1 cargo test -p hauksbee-engine --test strap_lint_corpus
cargo test -p hauksbee-ci --test boot_coverage
What each row rests on
The matrix's mechanisms are pinned by tests, split by whether they need boards on disk. The distinction matters, because a corpus-gated test that quietly passes with no corpus present would report green on zero coverage.
Needing no corpus, so runnable by anyone:
behavioral_framework: the behavioural device-model engine, covering pulls, FSM plus open-drain, expression laws, and converter regulation and limits.boot_coverage(hauksbee-ci): the synthetic two-sided AVR pair,gate_driven_promptly_passesandgate_left_floating_fails_naming_the_net, which is what proves the Hi-Z-control-input mechanism itself.checks::strapsin the engine lib suite: the synthetic two-sided strap discriminator and the GPIO15 rail-share regression.eagle_drc: the Eagle pour arbitration, including the rank cases and the jumper/net-tie exemptions.netlint: every true-positive and false-positive pair from the calibration, including the gold-row I2C shapes.powerup_state_fuzz: the spec machinery, with its corpus arms skipped.
Needing the corpus: the ZSWatch I2C gold row (known_faults), the ODrive,
MWGEN-G1 and emonTx copper rows including the V3.4.0 known-false-positive pin
(also known_faults), the Olimex strap
catches (strap_lint_corpus), the four behavioural power-IC rows
(behavioral_faults), and the Watchy QEMU boot-coverage pair. Absent a corpus
they early-return; HAUKSBEE_REQUIRE_CORPUS=1 makes a missing corpus a hard
failure instead, and the commands for that are in the reproduction section above.
Two honest bounds on that second group. Several of those tests address boards by
a board-corpus/famous/<id>/... layout that scripts/fetch-corpus.sh does not
produce, so run against a freshly fetched corpus they do not find their boards.
And the DISPLAY-EN row's faulty side needs ZSWatch DevKit 1.1.0, which
corpus.toml does not pin. Reproducing the matrix end-to-end therefore still
needs the maintainers' tree.