Hauksbee

About

Limitations: triage and closure

The full record, as it stands at docs/about/LIMITATIONS.md in the repository. File paths and test names in the text point into that tree, and the coverage matrix at the foot is assembled from the same backends this page describes.

This document states the structural and current implementation limits that bound a Hauksbee result. Surface-specific limits also appear beside the relevant commands and reports.

Structural ceilings

Two limits are not defects and will never appear under "closed". They bound the approach rather than the implementation, so the useful thing is to say where they bind and what is done about the part that can be.

Model availability caps analogue coverage

A circuit hauksbee cannot model, it cannot simulate. Many vendors ship SPICE and IBIS models only under NDA, or encrypted, and no amount of engineering here changes that. If a meaningful fraction of a design is unmodellable, coverage is capped by something outside the project.

Where it binds is lower than it first sounds, and the reason is what hauksbee asks. A transistor-level simulator needs the vendor's die model. Board-level CI asks whether a rail sags, whether a part runs past its rating, whether a pin conflicts, and whether the firmware drives what it thinks it drives. Those are answerable from terminal behaviour at datasheet grade: dropout, quiescent current, thermal resistance, logic thresholds, protection trip points. A behavioural model built from a public datasheet answers them, and it runs fast enough to sit in a commit hook, which an encrypted die model would not.

Three things follow from that, and all three ship:

The residue is real. Parts whose useful behaviour is genuinely not in the public datasheet, switching converters modelled past the averaged loop, and anything RF, stay out of reach. Those are named in the open section below and in CAPABILITIES.md rather than papered over.

One false positive costs more than one miss

A verification tool that cries wolf gets switched off, and a switched-off tool catches nothing. The bar for adoption is therefore higher than the bar for usefulness, and there is no second first impression. This is the sharpest risk the project carries.

A check that misfires on your board can be overruled one finding at a time, without switching the check off for everything else it catches. A waiver carries a required reason and a required expiry, so the finding comes back on a date rather than staying silenced forever, and waived findings are printed rather than hidden. See ../ci/CI.md.

The rest is aimed at the same thing. A check that fires must be right, and that is enforced rather than promised: every check is run across a corpus of real, working, shipped boards, and a check that lights up on a healthy board does not land, however good it is at finding the fault it was written for. See ../../CONTRIBUTING.md. Beyond that, a run that cannot produce a trustworthy answer exits 3 instead of passing, coverage holes travel as data fields rather than prose, and a report that would mislead says so instead of rendering.

What is honestly missing:

Open limitations

These are the genuine open limitations the code carries today, each with the reason it is open and what would close it.

Windows x86_64 has a narrower co-sim and host-serial shape

The native Windows artifact is permissive-only: Renode and Espressif QEMU are compiled in, while AVR/libsimavr is not. hauksbee doctor reports the exact backend paths it found, and the installer names this before download. The unlocking path for AVR is a maintained MSYS2 libsimavr build plus the same native lifecycle and firmware-through-hardware gates used by the shipped backends; until then no Windows artifact claims AVR support.

Windows also has no Unix pseudo-terminal endpoint. Host-facing UART traffic works over loopback TCP; a pty request refuses by name and points to --serial-transport tcp. Emulator children use a kernel Job Object, so this transport difference does not weaken process-tree cleanup. The browser front door, static analysis, CI verdicts, MCP, Renode/QEMU discovery, installation, and checksum-verified zip are native Windows surfaces. Emulator descendants are owned after a suspended child is attached to its Job Object. A hard parent kill in the narrow interval between process creation and Job assignment can leave that still-suspended direct child; Rust's stable Command API does not currently expose atomic Job assignment. The child has not executed or spawned descendants in that interval, and every normal/error/timeout/post-assignment hard-death path is structurally owned and tested.

A co-sim chunk no fallback can solve holds stale voltages

The co-sim advances the circuit in chunks. A chunk whose primary analog solve fails to converge is retried on a fallback ladder before the run gives up, in order of increasing desperation: the same integration with the maximum step bounded to a small fraction of the chunk, backward Euler at that bounded step, backward Euler from a cold start (the warm seed dropped so the operating point re-runs the full gmin and source-stepping continuation), and finally the chunk subdivided into quarters marched back to back. A chunk a rung carries is a real converged solve, and the run says which: the window and the rung that produced it are recorded per chunk and surfaced as fallback_windows in the co-sim JSON and the default text summary. A backward Euler window is first order and numerically dissipative, so fast transients and ringing inside it are damped relative to the second-order primary solve; the record states the method and carries a measured error_estimate_v, a conservative step-doubling estimate of the chunk-end voltage error obtained by re-solving the window at a shifted accuracy dial and differencing the end states (absent, never invented, when no companion re-solve converges). The typed error_budget partitions solved methods and marks unsolved spans invalid; it does not yet carry the per-window estimate, which today lives on the window record, the co-sim JSON, the CLI text summary, and the CI evidence assumptions.

What remains open is the chunk no rung can carry. Its node voltages are the previous chunk's, not a solved answer, so every voltage, current and fault reading inside that window is fiction, and the run says so rather than hiding it: a warning names the failed chunk count and the time spans, --strict turns it into a failing exit, and the web report raises it as a finding on the co-sim card. It will not invent a number for those windows. The usual cause there is structural rather than numerical: unresolved active parts leaving nodes floating (hauksbee models --help lists what is unresolved), a section with no reachable operating point, or conflicting rails, none of which any integration method can solve. Resolve the parts or simplify the offending section and re-run. The two-sided gate is crates/hauksbee-engine/tests/cosim_fallback_chunk.rs: a board whose fire step kills the primary march is rescued and recorded, and a structurally singular board still refuses loudly.

Six backend clock rates are verified; QEMU timing is approximate

Time-based co-sim results rest on the emulator advancing at the part's clock rate. simavr:atmega328p, renode:rp2040, renode:stm32f103, renode:stm32f4_discovery, renode:nrf52840, and renode:sifive_fe310 have clock-truth regressions in crates/hauksbee-mcu/tests/clock_truth.rs. Descriptors must declare a core clock consistent with frequency_hz.

ESP32 QEMU credits time from measured QMP RESUME/STOP windows, but virtual time remains approximate and degrades under host load. Affected runs carry a timing_limitation. STM32F103 TIMx blocks stay at the post-PLL 72 MHz required by stock HAL projects, and those runs disclose that constraint.

Parallel EEPROM programming models digital busy status, not charge-pump physics

The built-in AT28C256 path models the real bidirectional bus, final stored bytes, software protection, 64-byte page boundaries, and the 150 µs inter-byte deadline on cycle-exact simavr. It does not model the internal 3/10 ms charge- pump physics, endurance, retention, or optional 12 V chip erase. It does defer cell commits for the declared maximum program interval and returns the I/O7 data-polling complement and alternating I/O6 toggle bit while busy. The generic AT28C256 identity uses the conservative 10 ms maximum; a shorter delay must be justified by an explicitly resolved faster part, not inferred from the family.

An unserviced watchdog does not reset the MCU on most Renode parts or on QEMU

simavr models a starved wdt_enable(WDTO_15MS) by rebooting the core at the right virtual time, repeatedly. Reboots are reported rather than treated as a silent restart, because an assertion that passed across a reboot was not measuring the run it claimed.

Elsewhere the watchdog is a coverage hole and is surfaced as one per part. On renode:nrf52840 a watchdog arms and reads back as running with a correct 32768 Hz reload and never fires: zero resets in 1.000 s of simulated time where silicon gives twenty. On renode:stm32f103 the IWDG does fire and reset once, and the core then goes quiet where the part would reboot every timeout forever. On the ESP32 family the timer-group watchdogs are disabled at launch, which is the right call because a paused guest would otherwise trip them. On renode:stm32f4_discovery, renode:sifive_fe310 and renode:rp2040 nobody has run a starved watchdog to its timeout, and the two parts that were measured disagree with each other, so nothing is inferred. Firmware that hangs still runs forever on those parts, so any assertion about behaviour after a hang is fiction, but a run on them says so rather than reading healthy.

Power-up state is not modelled: no brownout reset, no strap latch, no fuses

Three related gaps, all on the digital side:

nRF5340 has no co-sim backend

The Renode 1.16.1 portable build ships no nRF5340 platform, so the ZSWatch-class DISPLAY-EN fault stays a static miss. nRF52840 is the closest proven platform. See docs/cosim/MCU.md.

PCB-only extraction has no pinfunctions

Multi-unit packages fall back to db pin maps when only a layout (no schematic netlist) is available. Schematic netlists are authoritative when present.

Device-decode is per-part and grows one part at a time

The configurable-controller decode check (config-pin divider vs the part's datasheet band table) has no generic engine. Each supported part is a hand-written decoder, seeded with the CYPD3177 USB-C PD sink only. It also does not read the silk-screened voltage label next to a rotary detent, so it reports which bands a selector can and cannot reach rather than "detent N labelled X codes Y". See docs/checks/DEVICE_DECODE.md.

Legacy KiCad-5 .sch (non-s-expression) reader

The Olimex ESP32-EVB revs A..K2 ship EESchema Schematic File Version 2 ASCII schematics. A reader for this format would need a second, independent schematic front-end: its own $Comp/$Sheet/wire/label parser, a fresh geometric netliser, and library-cache handling, all to the same exactness bar as the s-expr reader (the corpus standard is zero split/merged nets against the PCB). That is a large, self-contained effort with no shared code path with the s-expr reader, so it is deferred as the genuinely-hard, low-ROI item. The PCB extractor still handles those boards' layouts. Only their legacy schematics are out of scope.

I2C/SPI slave co-sim coverage, and what remains open around it

The working coverage first, because the boundary matters: I2C/SPI slave co-simulation intercepts the hardware TWI/SPI in-process on AVR, so the byte stream itself is exact (no timing model, no reimplemented controller); on Renode it runs through generated C# bridge peripherals on every platform whose SoC descriptor names bus controllers (STM32F103 i2c1/ spi1, STM32F4 Discovery i2c1/spi2-spi3, nRF52840 twi0/twi1/spi2, RP2040 i2c0/i2c1); and on QEMU-ESP32 it runs through a firmware mailbox contract. FE310 declares no bus controllers, and RP2040 declares none for SPI because the vendored PL022 model bit-bangs onto GPIO pins and never dispatches to a registered slave, so a bridge there would silently see nothing. A sensor bound to a bus with no controller is recorded as unexercised on every report surface, and a peripheral assertion against it fails rather than green-passing. Shipped proof: the i2c_sensor_cosim_renode.rs and spi_sensor_cosim_renode.rs integration tests in crates/hauksbee-engine/tests/.

What remains open:

KiCad 10 boards: exact native-DRC parity remains unvalidated

KiCad 10's format (20260206) uses name-only nets and represents baked antipad voids as keyholes in a filled contour. Both are handled. A focused fixture checked with kicad-cli 10.0.5 keeps a correctly isolated keyhole-antipad pad silent while reporting a pad under solid different-net fill, and the VENDETTA ESC oracle has no Zone-Pad findings.

The remaining limitation is narrower but still important: the complete finding set does not yet match native KiCad DRC exactly (VENDETTA reports 67 Hauksbee shorts versus 60 native shorting_items). Format versions at or above 20260000 therefore retain a version_warning; their findings are demoted and do not fail strict CI gates.

KiCad 10 project clearance rules, which live in the sibling .kicad_pro rather than in the board text, are read. The CLI and engine reports resolve them to the board's own net names before running DRC (kicad_pro_clearance_rules in crates/hauksbee-engine/src/reports/mod.rs, over clearance_rules_from_kicad_pro in crates/hauksbee-extract/src/drc.rs); a missing or malformed project file simply leaves DRC on the board/default rules. The narrower gap is at the library boundary: drc_from_text takes board text alone, so a caller driving that API directly has to read the project file and pass the rules through drc_from_text_with_clearance_rules itself. Cross-check them with KiCad 10's own DRC. Details and oracle evidence are in ../checks/SHORTS.md.

Eagle copper-pour fidelity in DRC

An Eagle .brd stores a signal pour's requested outline and all of its pour settings (isolate, rank, thermals, orphans, pour: all parsed); only the computed fill polygon is absent, because Eagle re-derives it on every ratsnest / CAM run. That derivation is what keeps the fill out of the pour-to-copper short test: Eagle carves max(isolate, the applicable design-rule / net-class clearance) around every foreign-net wire, pad and via (an isolate below the rules distance is ignored there; the pour-to-POUR case below is measured to behave differently), thermal spokes only remove same-net copper, and orphan removal only deletes fill pockets. Every setting keeps or widens gaps, so a correctly derived fill cannot short or crowd foreign copper in the same file. Treating the drawn outline as solid copper instead would manufacture false shorts on every trace the fill legitimately carves around. To be precise about what is and is not computed: the fill itself is never reconstructed and the isolate distance is never numerically re-verified; the settings are parsed, drive the reasoning above, and travel verbatim on a pour finding's Item::owner field, though --drc does not render that field. Pour-to-copper pairs are therefore not checked (rather than checked and found clean), and same-rank pours that approach each other without ring overlap are not distance-checked either, since the fill extent near the boundary depends on those settings. The argument above says a fill Eagle derives from these settings would not violate; a hand-edited file whose fill was never re-derived is outside it.

One pour-to-pour construct IS checked: two overlapping same-rank pours of different signals get no arbitration from their rank, and the overlap is reported. Its error rate is measured rather than assumed, because the emonTx revision family ships fabrication output beside the design and so can be scored. Across the six layer-instances with copper gerbers, the rule is right about four: it flags the three layers where the two nets really do share copper, and over-reports two top layers where the outlines overlap and nothing bridges them. The scoring table is in ../evidence/CORPUS.md.

The two over-reports, and why no threshold removes them. Whether two overlapping pours end up in contact is a property of the fill, and the .brd does not carry the fill. isolate is not a substitute: it is necessary but not sufficient, which is measured, because the emonTx V3.4.0 sets BOTH of its top-layer pours to 0.00030625 with overlapping outlines and their fills still come out as separate components. A narrowing that reported the overlap only below a one-micrometre isolate was implemented and reverted for that reason: it fixed one of the two over-reports and silenced a third layer where a trace genuinely joins the two nets, which trades a false positive for the worse kind of error (see SHORT_TOUCH_EPS_MM in drc.rs on under-reporting a short). Closing them properly needs Eagle's fill reconstructed from the outline, the pour settings and the foreign copper, which is not implemented.

A tie the .brd cannot declare, read from the .sch. The three true reports on that family are all a ground tie the designer DECLARED: emonTx V3.4.5.sch wires an AGND supply symbol to a GND supply symbol in one segment of net GND, from V3.4.1 onward (V3.4.0 does not, and the parser reports 0 ties there against 1 from V3.4.1 on). They are true about the copper. The schematic establishes net-pair intent but carries no board coordinate that can prove either observed contact is deliberate, so both remain SERIOUS without board-local authority. The Eagle net-tie exemption keys on jumper libraries and package conventions (../checks/SHORTS.md), which cannot express that, and the declaration is in the .sch while the DRC is handed the .brd.

That is now a supported contextual upload rather than a limitation. Supply the schematic (--schematic <FILE>, or leave a matching valid sibling beside the board) and the contact keeps its layer, location, measured gap, SERIOUS severity and --strict gate, while gaining the exact declaration (AGND7 wired to SUPPLY6 in net GND) and schematic provenance. Board-local authority, such as a named net-tie footprint or reviewed coordinate, is still required to downgrade a physical finding. With no schematic, the report names it as an input that may explain net-pair intent, never as sufficient authority to clear the short.

The recogniser is narrow, and it has to be. A declaration comes from Eagle's own supply-symbol construct, admitted only when the library symbol has exactly one pin carrying direction="sup", its deviceset has one gate, and no device behind it names a package: a supply symbol is a schematic-only marker, not a part. Ordinary libraries mark a real component's power pins sup, so an any-sup-pin rule turned an SD socket and an XBEE module into blanket ground exemptions, 6 false declarations on margay_logger and 19 on emonTx V3.2 including 3.3V to GND, each able to attach false context to a genuine rail-to-ground short. The always-run contract is the tracked declared/undeclared pair under crates/hauksbee-extract/tests/fixtures/eagle_ties/, plus focused parser tests for multi-pin, packaged, URN-collision and structural-scope false positives.

An explicit companion must share the board basename and exactly match the board's physical reference/value set and package-pad/net incidence. One declaration may add context to a unique same-location contact cluster (all copper layers at that point), but never authorizes it. Multiple spatial contacts get no declaration context because the schematic cannot identify which one it describes. See ../checks/SHORTS.md.

Scoped to Eagle. A .kicad_pcb declares its ties in the layout the DRC already has (net_tie_pad_groups, (attr net_tie)) and a .kicad_sch has no construct for a deliberate two-net join to read (see ../ingest/SCHEMATICS.md, "Net-tie footprints ... have no schematic counterpart"). Altium carries the native COMPONENTTYPE=Net Tie field. Eagle was the one format with the hole.

That overlap keys on the polygons' vertex rings: same-rank pours whose rings miss by less than their drawn boundary stroke widths are not flagged, and pinning whether Eagle treats a stroke graze as overlap needs an Eagle-generated oracle board the corpus does not yet carry. Wires, vias and pads against each other are fully covered.

Gerber footprint inference on stripped films (via-vs-pad ambiguity)

An X2 gerber job carries the answer in the files: %TO.P names each pad's refdes and pin, %TO.N names its net, and %TA.AperFunction,ViaPad marks a via as a via. The reader uses all three, so on an X2 export (KiCad's default) pad→refdes→pin→net binding and via-vs-pad classification come from the film itself, exactly: no window, no inference.

The limitation is now confined to stripped films (exports run with --no-x2 --no-netlist, or legacy CAM output that never carried attributes). There the reconstruction windows each component's pads from its package-name string, with a flat 4 mm fallback, and a stitching/thermal via inside that window is hard to tell from a real pad, which inflates component pad counts. The connectivity the simulator needs is already near-exact over located pads (net-partition agreement runs to about 99% on the gated boards), so the residual is a component-match accounting figure, not a correctness gap. If your job hits it, the unlocking upload is one of: re-export the gerbers with X2 attributes enabled (KiCad: don't pass --no-x2/--no-netlist; Altium: tick "Generate X2 attributes"), or supply the native layout file alongside the fab folder.

Capacitor ESR/ESL parasitics stay opt-in by design

Parasitics are deliberately not on by default. Enabling them changes transient and operating-point results, so a run must opt in explicitly even when the component class is inferred confidently.

Co-simulation coverage, by MCU family

The static checks and the analogue solve read the board, so they need no emulator and run whatever the MCU is, or if the board has none. Everything to the right of them runs firmware on an emulated part, so it is only as good as the backend for that part.

MCU familyStatic checksAnalogue solveCo-sim bootGPIO and UARTADC injection
AVR, ATmega328Pyesyesyesyesyes, AVR-exact
STM32, F103yesyesyesyesnone: the stock Renode platform models no ADC, so injections drop and warn
ESP32 and ESP32-C3yesyesyesyesyes, through the firmware mailbox contract
ESP32-S3yesyeswiring proven: the machine boots and the channels connect; app proof waits on an S3 imagesame imagesame contract as the ESP32, unproven on S3
nRF52840yesyesboot-only: a Zephyr shell prompt through the bridgeUART proven; the GPIO bridge is the STM32-proven poll, not yet run on nRFnone: the live platform models no SAADC
RISC-V, SiFive FE310yesyesboot-only: Zephyr to its shell through the bridgeUART proven, but there is no verified drive-direction map, so the GPIO bridge is direction-blindnone
nRF5340yesyesno backend yet; nearest proven part is the nRF52840no backend yetno backend yet

Analogue-converter injection is exact on AVR only, and stock Renode platforms refuse an injection out loud rather than guessing at one. Two caveats sit under the analogue solve itself, whatever the MCU: the AC sweep is averaged about the operating point rather than switched cycle by cycle, and the thermal pass is a per-device junction temperature rather than a board field solve.