Skip to content

framework: passive + diode terminals mandatory - #30

Closed
raeq wants to merge 4 commits into
framework-led-cathode-mandatoryfrom
framework-passive-terminals-mandatory
Closed

framework: passive + diode terminals mandatory#30
raeq wants to merge 4 commits into
framework-led-cathode-mandatoryfrom
framework-passive-terminals-mandatory

Conversation

@raeq

@raeq raeq commented May 19, 2026

Copy link
Copy Markdown
Owner

Summary

Continuation of #29. The same physical-fidelity violation that left LED.cathode floating was also present on every other passive component: Resistor, Capacitor, Inductor, and every diode subclass had their terminals declared mandatory=False with the "structurally inert under graph evaluation" shortcut. Real components with dangling terminals do nothing — they sit on the BOM but contribute nothing to the circuit.

  • Resistor.t1/t2, Capacitor.t1/t2, Inductor.t1/t2, and every Diode subclass's anode/cathode now mandatory=True.
  • Six demos (backup_power, bldc_motor, dice, doorbell_protector, fan_cooling, isolated_rs232) declared BOM-only passives that were never wired. Each now wires them either to their datasheet-correct net (e.g. DoorbellProtector's flyback diode anode joining the relay's coil_minus) or as an isolated 0-Ω passthrough with dynamically_driven=True where the part is genuinely BOM-only and the voltage-only simulator can't model its behaviour. Comments at each call site explain which.
  • New regression tests:
    • tests/components/test_resistor.py, test_capacitor.py, test_inductor.pytest_terminals_are_mandatory + test_floating_*_refused_at_circuit_construction
    • tests/components/test_diode_mandatory_terminals.py (new file) — parametrized across all 7 diode subclasses

After this PR, the breadboard SVGs from #28 show every BOM-listed part connected to something — no more orphaned caps and resistors floating without jumpers.

Test plan

  • uv run pytest — 4382 passed (20 new regression tests).
  • uv run mypy src/ demos/ clean.
  • All demo docs regenerated; per-format goldens refreshed where the rewired demos produced different output.

Based on #29.

Render any wirebench design as it would sit on a standard solderless
breadboard. SVG output sibling to the assembly-guide markdown: the
two artefacts share `assembly_guide.placement.place()` so the
positions named in the assembly guide's step-by-step instructions
match the components' positions in the rendered SVG (acceptance
criterion #8 of `.plans/phase-2.6-spec.md`).

Subpackage `src/framework/export/breadboard/`:
  - colors.py — locked palette (red reserved for + rail, black for −,
    4-tone analog & digital signal cycles). Cycles are ordinal, not
    hash-based: adjacent nets on the board get adjacent colours so
    physically-close jumpers can't collide.
  - svg.py — coordinate system, inline-style SVG primitives, dynamic
    canvas width (grows for designs over 63 columns), dynamic
    viewBox y-offset for designs needing many detour bands.
  - placement.py — refusal of SMD / Board / multi-Board designs;
    wraps `assembly_guide.placement.place()`.
  - routing.py — net walker producing coloured Jumper records.
  - renderer.py — interval-coloured detour-band allocation, double-
    detour for cross-bank jumpers (pivots at clear columns so the
    wire never traverses a chip body's opposite-bank tie strip),
    pin-name labels on chip bodies, end-cap discs at every plug-in
    point, per-endpoint row assignment so chained nets land in
    distinct holes on a shared tie strip.

Z-order: surface → jumpers → components, so any jumper section that
would pass behind a chip is hidden by the opaque body — visually
representing how a real jumper physically routes around the chip.

Spec deferrals (`.plans/phase-2.6-spec.md` §12): full-size 63-column
board (assembly_guide places parts past col 30, half-size impossible
in practice); multi-board composite SVG (multi-Board designs raise
`BreadboardIncompatibleError`, matching assembly_guide).

Other changes:
- `assembly_guide.placement._place_2lead_part` now lands passive
  leads on row D (near the trough) instead of row A. Leaves rows
  A–C clear for jumpers terminating on the same tie strip; the
  assembly-guide goldens stay byte-identical because they use the
  `any of <pos>A–<pos>E` tie-strip phrasing.
- `scripts/render_demo_docs.py` adds `breadboard` to its format loop
  and emits an SVG refusal stub for designs the visualiser refuses.
- 26 new tests under `tests/framework/export/breadboard/` (placement,
  routing, renderer, byte-identical goldens for 5 demos).
- Main README and 12 per-demo READMEs reference the new format.
- Every demo's `docs/` folder gains a `<Design>.breadboard.svg`.
Copilot AI review requested due to automatic review settings May 19, 2026 15:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens the framework’s “physical fidelity” enforcement by making passive-component terminals and all diode terminals mandatory, so circuits that list parts in the BOM but leave their pins floating are rejected at Circuit construction time. It also updates affected demos/fixtures and refreshes exported/golden artifacts so every declared part is connected (or explicitly modeled as a “BOM-only” 0‑Ω passthrough where the voltage-only evaluator can’t represent the real analog behavior).

Changes:

  • Make Resistor.t1/t2, Capacitor.t1/t2, Inductor.t1/t2, and all diode anode/cathode ports mandatory=True.
  • Update demos and test fixtures to wire previously-floating passives/diodes (either to a real net or as isolated/rail passthroughs using dynamically_driven=True where required).
  • Add regression tests ensuring floating passives/diodes raise UnconnectedPinError, and refresh many generated export/golden files impacted by the rewiring.

Reviewed changes

Copilot reviewed 102 out of 115 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/components/passives/resistor.py Make resistor terminals mandatory (mandatory=True) and update rationale comment.
src/components/passives/capacitor.py Make capacitor terminals mandatory and update rationale comment.
src/components/passives/inductor.py Make inductor terminals mandatory and update rationale comment.
src/components/diodes/d1n4001.py Make diode terminals mandatory.
src/components/diodes/d1n4007.py Make diode terminals mandatory.
src/components/diodes/d1n4148.py Make diode terminals mandatory.
src/components/diodes/d1n4728a.py Make diode terminals mandatory.
src/components/diodes/d1n4733a.py Make diode terminals mandatory.
src/components/diodes/d1n4742a.py Make diode terminals mandatory.
src/components/diodes/d1n5817.py Make diode terminals mandatory.
tests/components/test_resistor.py Add regression tests asserting terminals are mandatory + floating resistor rejected.
tests/components/test_capacitor.py Update expectations for mandatory ports + add floating capacitor rejection test.
tests/components/test_inductor.py Update expectations for mandatory ports + add floating inductor rejection test.
tests/components/test_diode_mandatory_terminals.py New parametrized regression tests for all diode subclasses + floating diode rejection.
tests/framework/test_refdes.py Adjust tests to wire mandatory resistor terminals so refdes validation is reached.
tests/framework/test_format_roundtrip.py Update hand-edited .wirebench fixture to include a rail and wires satisfying mandatory pins.
tests/framework/import_kicad/fixtures/regulator_with_named_rail.net Fixture update reflecting added passive connectivity.
tests/framework/export/bom/test_bom_components.py Update BOM export test to include wiring that satisfies mandatory pins.
tests/framework/export/dot/test_dot_components.py Update DOT export test to include wiring that satisfies mandatory pins.
tests/framework/export/mermaid/test_mermaid_components.py Update Mermaid export test to include wiring that satisfies mandatory pins.
tests/cli/fixtures/floating_net.py Update CLI fixture to satisfy mandatory pins while still triggering FloatingNetError.
demos/backup_power/backup_power.py Wire BOM-only passives as 0‑Ω passthroughs on VCC rail to satisfy mandatory-pin enforcement.
demos/doorbell_protector/doorbell_protector.py Wire previously BOM-only passives/diode terminals (incl. synthetic dynamically-driven net for coil clamp node).
demos/dice/dice.py Wire BOM-only timing/decoupling passives as 0‑Ω passthroughs on VCC to satisfy mandatory pins.
demos/bldc_motor/bldc_motor.py Wire BOM-only decoupling/bootstraps + reset pull-up as isolated dynamically-driven passthrough nets.
demos/fan_cooling/fan_cooling.py Wire BOM-only passives/diode as isolated dynamically-driven passthrough nets (avoid graph feedback).
demos/isolated_rs232/isolated_rs232.py Wire BOM-only caps as isolated dynamically-driven passthrough nets.
demos/backup_power/docs/BackupPower.* Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs).
demos/doorbell_protector/docs/DoorbellProtector.* Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs/assembly guide).
demos/dice/docs/Dice.* Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs/breadboard).
demos/fan_cooling/docs/FanCoolingBoard.* Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs/schematic).
demos/fan_cooling/docs/CooledSystem.* Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs).
demos/isolated_rs232/docs/IsolatedRS232Link.* Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs).
demos/isolated_rs232/docs/IsolatedRS232Board.* Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs).
demos/bldc_motor/docs/BLDCSystem.* Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs).
demos/bldc_motor/docs/BLDCControllerBoard.* Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs).
tests/golden/assembly_guide/dice.md Golden refresh for dice assembly guide due to added wiring steps.
tests/golden/assembly_guide/doorbell_protector.md Golden refresh for doorbell protector assembly guide due to added wiring steps.
tests/golden/breadboard/dice.breadboard.svg Golden refresh for dice breadboard SVG due to added pass-through wiring.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

subzero added 2 commits May 19, 2026 18:04
- Rail jumpers always route to the TOP rails. The shared assembly
  guide instructs builders to wire only the top `+`/`-` rails to
  the supply; standard breadboards keep top and bottom rail strips
  electrically isolated, so a chip pin on the bot bank routing to
  `+BOT`/`-BOT` would show an unpowered connection. Bot-bank pins
  now cross the trough via the standard cross-bank double-detour,
  terminating at the powered top rail. Same-column rail verticals
  (DIP VCC pin at col X to +TOP at the same col X) get hidden in
  the middle by the chip body's z-order, visually representing the
  wire arching over the package.

- Refusal docstring updated to describe the actual capacity limit:
  designs that exceed `svg.MAX_POSITIONS` (four daisy-chained
  full-size breadboards) are refused. Designs that just exceed one
  board's 63 columns (Dice → 106, DoorbellProtector → 92) are
  rendered on a canvas wide enough to fit them, with a faint
  column-break marker every 63 columns.

Breadboard goldens for WaterAlarm, Dice, DoorbellProtector
refreshed; demo SVGs regenerated.
Found while reviewing WaterAlarm's breadboard SVG: the LEDs in the
design had no ground connections drawn. Investigation revealed
`LED.cathode` was declared `mandatory=False` with the docstring claim
*"floating = GND"* — a "structurally inert under graph evaluation"
shortcut that lets demos write electrically-broken circuits and have
them pass ERC.

A real LED with a dangling cathode does not light. This is exactly
the kind of physical-fidelity violation `CLAUDE.md` lists as
disallowed.

  - `src/components/passives/led.py`: `cathode` becomes
    `mandatory=True`. Docstring updated to drop the "floating = GND"
    claim and explain the contract.
  - `tests/components/test_led.py`: regression tests pin both ports
    as mandatory and confirm `Circuit.__init__` raises
    `UnconnectedPinError` end-to-end when a cathode is left
    floating.
  - `demos/water_alarm/water_alarm.py` and
    `demos/water_alarm_split/water_alarm_split.py`: cathodes wired
    to the GND rail. (Bench builds still need a series current-limit
    resistor; the logic-level demo doesn't model V_F or current.)
  - `tests/framework/test_circuit_autocollect.py`,
    `tests/framework/test_framework.py`, and
    `tests/framework/import_kicad/test_import.py`: fixtures that
    leaned on the broken default updated so they still pin down the
    specific error each test exists to catch.
  - Demo docs regenerated for the two WaterAlarm variants.
@raeq
raeq force-pushed the framework-led-cathode-mandatory branch from 89570e4 to a265d46 Compare May 19, 2026 16:05
Continuation of the LED.cathode regression fix (#29). Resistor,
Capacitor, Inductor, and every Diode subclass had their terminals
declared `mandatory=False` with the same "structurally inert under
graph evaluation" justification — letting demos declare BOM-only
timing / decoupling / pull-up parts without wiring them, then ship
breadboard SVGs and netlists where those parts sat orphaned.

A real resistor / capacitor / inductor / diode with a dangling
terminal does nothing. Marking the ports mandatory closes the
silent regression at the framework layer.

  - `src/components/passives/{resistor,capacitor,inductor}.py` and
    every diode subclass under `src/components/diodes/` now declare
    their terminals `mandatory=True`.
  - `tests/components/test_{resistor,capacitor,inductor}.py`:
    regression tests pin the new contract per component.
  - `tests/components/test_diode_mandatory_terminals.py` (new):
    parametrized over D1N4001 / D1N4007 / D1N4148 / D1N4728A /
    D1N4733A / D1N4742A / D1N5817.
  - Six demos (`backup_power`, `bldc_motor`, `dice`,
    `doorbell_protector`, `fan_cooling`, `isolated_rs232`) had
    declared-but-unwired passives — each now wires them either to
    their datasheet-correct net (e.g. DoorbellProtector's flyback
    diode anode joins the relay coil_minus net) or as an isolated
    0-Ω passthrough with `dynamically_driven=True` where the part
    is genuinely BOM-only and the voltage-only simulator can't
    honour its behaviour. Comments at each call site explain
    which kind.
  - Test fixtures across CLI, refdes, format-roundtrip, kicad
    import, and per-format renderer tests updated so they still
    pin down the specific error each one exists to catch without
    tripping the new mandatory-pin check first.
  - All demo docs regenerated; goldens refreshed.

After this commit the breadboard SVGs show every BOM part connected
to something — no more orphaned caps and resistors floating without
jumpers.
@raeq
raeq force-pushed the framework-passive-terminals-mandatory branch from 07ab539 to 04ef6e3 Compare May 19, 2026 16:06
@raeq
raeq force-pushed the framework-led-cathode-mandatory branch from a265d46 to 2f70dab Compare May 19, 2026 16:32
@raeq
raeq deleted the branch framework-led-cathode-mandatory May 19, 2026 16:34
@raeq raeq closed this May 19, 2026
raeq pushed a commit that referenced this pull request May 19, 2026
Continuation of the LED.cathode regression fix (#29). Resistor,
Capacitor, Inductor, and every Diode subclass had their terminals
declared `mandatory=False` with the same "structurally inert under
graph evaluation" justification — letting demos declare BOM-only
timing / decoupling / pull-up parts without wiring them, then ship
breadboard SVGs and netlists where those parts sat orphaned.

A real resistor / capacitor / inductor / diode with a dangling
terminal does nothing. Marking the ports mandatory closes the
silent regression at the framework layer.

  - `src/components/passives/{resistor,capacitor,inductor}.py` and
    every diode subclass under `src/components/diodes/` now declare
    their terminals `mandatory=True`.
  - `tests/components/test_{resistor,capacitor,inductor}.py`:
    regression tests pin the new contract per component.
  - `tests/components/test_diode_mandatory_terminals.py` (new):
    parametrized over D1N4001 / D1N4007 / D1N4148 / D1N4728A /
    D1N4733A / D1N4742A / D1N5817.
  - Six demos (`backup_power`, `bldc_motor`, `dice`,
    `doorbell_protector`, `fan_cooling`, `isolated_rs232`) had
    declared-but-unwired passives — each now wires them either to
    their datasheet-correct net (e.g. DoorbellProtector's flyback
    diode anode joins the relay coil_minus net) or as an isolated
    0-Ω passthrough with `dynamically_driven=True` where the part
    is genuinely BOM-only and the voltage-only simulator can't
    honour its behaviour. Comments at each call site explain
    which kind.
  - Test fixtures across CLI, refdes, format-roundtrip, kicad
    import, and per-format renderer tests updated so they still
    pin down the specific error each one exists to catch without
    tripping the new mandatory-pin check first.
  - All demo docs regenerated; goldens refreshed.

After this commit the breadboard SVGs show every BOM part connected
to something — no more orphaned caps and resistors floating without
jumpers.
raeq added a commit that referenced this pull request May 19, 2026
Continuation of the LED.cathode regression fix (#29). Resistor,
Capacitor, Inductor, and every Diode subclass had their terminals
declared `mandatory=False` with the same "structurally inert under
graph evaluation" justification — letting demos declare BOM-only
timing / decoupling / pull-up parts without wiring them, then ship
breadboard SVGs and netlists where those parts sat orphaned.

A real resistor / capacitor / inductor / diode with a dangling
terminal does nothing. Marking the ports mandatory closes the
silent regression at the framework layer.

  - `src/components/passives/{resistor,capacitor,inductor}.py` and
    every diode subclass under `src/components/diodes/` now declare
    their terminals `mandatory=True`.
  - `tests/components/test_{resistor,capacitor,inductor}.py`:
    regression tests pin the new contract per component.
  - `tests/components/test_diode_mandatory_terminals.py` (new):
    parametrized over D1N4001 / D1N4007 / D1N4148 / D1N4728A /
    D1N4733A / D1N4742A / D1N5817.
  - Six demos (`backup_power`, `bldc_motor`, `dice`,
    `doorbell_protector`, `fan_cooling`, `isolated_rs232`) had
    declared-but-unwired passives — each now wires them either to
    their datasheet-correct net (e.g. DoorbellProtector's flyback
    diode anode joins the relay coil_minus net) or as an isolated
    0-Ω passthrough with `dynamically_driven=True` where the part
    is genuinely BOM-only and the voltage-only simulator can't
    honour its behaviour. Comments at each call site explain
    which kind.
  - Test fixtures across CLI, refdes, format-roundtrip, kicad
    import, and per-format renderer tests updated so they still
    pin down the specific error each one exists to catch without
    tripping the new mandatory-pin check first.
  - All demo docs regenerated; goldens refreshed.

After this commit the breadboard SVGs show every BOM part connected
to something — no more orphaned caps and resistors floating without
jumpers.

Co-authored-by: subzero <subzero@noreply.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants