wirebench is a Python framework that refuses to construct a circuit whose wires wouldn't physically work. Contributions land best when they preserve that property — the framework's value is the defects it makes unconstructible, not the features it adds.
This document is the road map. Deeper material lives in
docs/ (durable design reference, user-curated) and
.plans/ (Claude-generated implementation specs and
audits — local-only, gitignored).
Read docs/design-principles.md before
non-trivial changes. The short version:
- Physical fidelity is primary. Every class represents a physical thing. If you couldn't do it with a soldering iron, you can't do it in code.
- Components are callable.
__call__is the sole signal interface; no setters, no mutator methods. - Invalid states raise. Hardware has forbidden states (S=R=1,
shoot-through, reversed-polarity electrolytics) — they raise leaves
from
framework.errors, never bareValueError/TypeError. __slots__everywhere. Physical parts don't grow pins at runtime.
A Chip subclass whose ports include an OUT pin must drive that
pin's internal face via a cell — or declare BARE_FIRMWARE_DRIVEN = True (the opt-out for MCUs whose outputs are firmware-driven). The
Chip.__init__ check enforces this at construction; a misconfigured
chip can't be instantiated.
When adding a new chip, the canonical pattern is in
src/components/chips/concepts/:
the chip class composes one or more behavioural cells, wires its OUT
pins to cell outputs, and passes the cells in parts=[...]. See the
behavioural-cell audit spec
for the full pattern.
The recommended path for a new component is the scaffold script. It
machine-applies every contributor-side rule (__slots__, the six
required ClassVars, @register, refdes validation, port shape, test
stub) so you can focus on the part-specific specification.
uv run scripts/scaffold_component.py \
--name LM7806 \
--kind chip \
--refdes-prefix U \
--footprint "Package_TO_SOT_THT:TO-220-3_Vertical" \
--pins "vin:in:Analog,gnd:in:Analog,vout:out:Analog" \
--description "6 V linear regulator — TO-220 fixed-output."This emits two files:
src/components/chips/lm7806.py— the component class, with__slots__, every required ClassVar (REFDES_PREFIX,FOOTPRINT,PIN_NUMBERS,LAYOUT,VERIFY,GOTCHAS),@register, a refdes-validating__init__, and a placeholderevaluate()/__call__()shape that drives every OUT pin so the framework's OUT pin must be driven invariant passes by default.tests/components/test_lm7806.py— a construction-shape test stub that asserts the class refdes, port surface, and per-pin direction + signal-type values.
The scaffold also re-exports the new class from the kind's
__init__.py so from components.chips import LM7806 works.
You then fill in:
- The class docstring — the part's real behavioural description, pin table, operating range, framework-relevant gotchas.
- The
VERIFYstrings — multimeter / bench-test instructions the builder runs before powering the board. - The
GOTCHASstrings — assembly-time warnings theassembly_guideexporter surfaces to the breadboard builder. - The
LAYOUTdescriptor (axial_2lead, dip, qfp, …) so the breadboard SVG visualiser knows how to draw the part. - The real
evaluate()/__call__()logic. For chips with OUT pins, the canonical pattern is a concept cell undersrc/components/chips/concepts/: instantiate the cell in__init__, wire it to the OUT pin's.internalface, let auto- collect pick it up viaself.cell = MyConcept(...).
Supported --kind values today are passive and chip. For other
families (connector, diode, transistor, relay, transducer),
the base classes have shapes too varied to template usefully — copy
an existing example (src/components/diodes/, src/components/connectors/,
etc.) and adapt. The framework rules apply equally to hand-written
components.
If you're not using the scaffold, the manual steps are:
- Pick the right base class:
Chipfor ICs (anything with internal logic + a pin table)Diode/Transistorfor those primitivesConnectorfor through-hole / SMD / cable connectors with a defined mate- Subclass directly from
Partonly for genuinely new categories
- Declare the pin table as
PIN_NUMBERS = {'name': number, ...}per the manufacturer's datasheet. - Set
REFDES_PREFIXper IEEE 315 (R,C,U,Q,D,J,K, etc.). - Set
FOOTPRINTto a KiCad footprint string (used by the KiCad exporter). BrowseDiode_THT:*,Package_TO_SOT_THT:*, etc. - If the part has OUT pins: write a concept cell in
src/components/chips/concepts/, instantiate it in the chip's__init__, wire it to the OUT pin's.internalface. - Add a pin-number test in
tests/components/test_chip_pin_numbers.py. - Add a construction test in
tests/components/.
The framework's auto-collect machinery picks up self.<name> part
attributes — so self.cell = MyConceptCell(...) is enough; no
explicit parts=[...] is needed for most subclasses.
docs/— durable, user-curated reference. Don't edit without an explicit ask. The user decides what's published there..plans/— implementation specs, audits, status reports (Claude-authored working docs). Gitignored. Local-only.
If you're writing a plan, spec, audit, or report, it goes in
.plans/. If it's reference material a contributor would search for
months from now, it goes in docs/.
uv sync --extra dev
uv run pytest # full suite (3861 tests)
uv run pytest -k <pattern> # subset
uv run mypy src/ demos/ # strict type-check
./scripts/cov.sh # full coverage reportThe fast feedback loop is plain uv run pytest; coverage is opt-in
via the script because pytest-cov and mutmut don't co-exist (see
the comment block in pyproject.toml).
Several renderers (KiCad, yosys, SPICE, dot, mermaid, BOM) emit byte-
exact output that's compared against checked-in goldens under
tests/golden/. When a legitimate behaviour change shifts the
output, regenerate with UPDATE_GOLDEN=1 uv run pytest <test_path>
and review the diff — if it contains anything beyond the change you
intended, investigate before committing.
Per-demo doc exports under demos/*/docs/ are similarly emitted by
scripts/render_demo_docs.py.
The framework enforces IEEE 315 reference-designator prefixes. The
canonical list lives in src/framework/refdes.py. A RefdesError at
construction usually means a REFDES_PREFIX on the class doesn't
match what the registry expects.
If pytest, mypy, or any other tooling reports an error — even one
you didn't author — fix it as part of the PR. The repo's standard is
that errors aren't negotiable; tolerating one quickly becomes
tolerating ten.
- Open a PR against
main. Thetestsandtypecheckworkflows must pass before merge. - The PR template is in
.github/PULL_REQUEST_TEMPLATE.md— it's a short checklist; please fill it in. - Squash on merge is fine; the commit message should describe the why, not just restate the diff.
Tagged releases (v0.X.Y on main) trigger
.github/workflows/release.yml, which builds the wheel and sdist,
publishes to PyPI via a Trusted Publisher (OIDC; no stored API keys),
and creates a GitHub release with auto-generated notes. The workflow
refuses to publish unless the tag matches pyproject.toml's
version field.