Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions docs/the-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ If two ports declared `Direction.OUT` end up on the same logical net, `wire()` (

**Why:** Two OUT-direction ports on one net fight each other on the copper. Current sinks through the losing output stage until the FETs overheat. Real silicon has one driver per shared conductor.

**First caught at:** [`hello_led/` — *A shorted supply*](../demos/hello_led/README.md#a-shorted-supply).
**First caught at:** [`hello_led/` — *A shorted supply*](https://github.com/raeq/wirebench/blob/main/demos/hello_led/README.md#a-shorted-supply).

## Rule 2 — Every BIDIR-only net needs a driver

Expand All @@ -27,15 +27,15 @@ A net touched only by passive BIDIR ports (resistor terminals, capacitor leads,

You can opt out with `wire(*ports, dynamically_driven=True)` when the net is driven through the surrounding loop (op-amp positive feedback, RC timing networks). The opt-out is the designer's explicit assertion that the framework's static check should yield to a known dynamic driver.

**First caught at:** [`hello_led/` — *A floating resistor*](../demos/hello_led/README.md#a-floating-resistor).
**First caught at:** [`hello_led/` — *A floating resistor*](https://github.com/raeq/wirebench/blob/main/demos/hello_led/README.md#a-floating-resistor).

## Rule 3 — Mandatory pins must be connected

Some pins are declared `mandatory=True` because the part doesn't function without them — regulator inputs, op-amp V+ supplies, MCU VDD, MCU GND, two-lead passive terminals. Leaving any of them unwired raises [`UnconnectedPinError`](https://github.com/raeq/wirebench/blob/main/src/framework/errors.py).

**Why:** A mandatory pin left in air leaves the silicon stage tied to nothing. The part either doesn't power up at all (no current path) or behaves unpredictably (floating reference). The bench equivalent is a board that arrives, populates, and refuses to come on — every solder joint is fine, but one wire was missing in the schematic.

**First caught at:** [`penfold_light_switch/` — *A floating LDR*](../demos/penfold_light_switch/README.md#a-floating-ldr) (the LDR's two terminals are mandatory; forgetting one raises `UnconnectedPinError`).
**First caught at:** [`penfold_light_switch/` — *A floating LDR*](https://github.com/raeq/wirebench/blob/main/demos/penfold_light_switch/README.md#a-floating-ldr) (the LDR's two terminals are mandatory; forgetting one raises `UnconnectedPinError`).

## Rule 4 — Signal types stay matched

Expand All @@ -45,7 +45,7 @@ Every port carries one of two signal families: `Analog` (continuous voltage) or

A BIDIR port declared as the generic `Analog` base class acts as a *conductor wildcard* — a piece of copper that takes on whatever type the rest of the wire imposes. This is what lets connector contacts and resistor terminals join either domain without breaking the discipline.

**First caught at:** [`penfold_light_switch/` — *A mismatched comparator input domain*](../demos/penfold_light_switch/README.md#a-mismatched-comparator-input-domain).
**First caught at:** [`penfold_light_switch/` — *A mismatched comparator input domain*](https://github.com/raeq/wirebench/blob/main/demos/penfold_light_switch/README.md#a-mismatched-comparator-input-domain).

## Rule 5 — Ground domains stay isolated

Expand All @@ -55,23 +55,23 @@ A `wire()` (or any port-to-node attachment) that crosses two distinct `GroundDom

The framework allows isolator cells (e.g. `ISOW7841`, `Optocoupler`) to have ports in different domains as the legitimate way to bridge.

**First caught at:** [`isolated_rs232/` — *A cross-domain wire*](../demos/isolated_rs232/README.md#a-cross-domain-wire).
**First caught at:** [`isolated_rs232/` — *A cross-domain wire*](https://github.com/raeq/wirebench/blob/main/demos/isolated_rs232/README.md#a-cross-domain-wire).

## Rule 6 — Connectors only mate with their declared partner

Every connector class declares its physical mate via `MATES_WITH`. Calling `mate(a, b)` where `type(b)` isn't `type(a).MATES_WITH` raises [`IncompatibleMateError`](https://github.com/raeq/wirebench/blob/main/src/framework/errors.py).

**Why:** A USB-A receptacle and a TRRS audio jack have different shells, different pin counts, different pitches. Calling them mated is asserting a fact contradicted by the mechanical drawings. The bench equivalent is a parts order with the wrong cable type — the cable arrives and won't seat. The framework catches the mismatch when the code says `mate()`, before the order ships.

**First caught at:** [`water_alarm_split/` — *A wasted parts order — wrong connector family*](../demos/water_alarm_split/README.md#a-wasted-parts-order--wrong-connector-family).
**First caught at:** [`water_alarm_split/` — *A wasted parts order — wrong connector family*](https://github.com/raeq/wirebench/blob/main/demos/water_alarm_split/README.md#a-wasted-parts-order--wrong-connector-family).

## Rule 7 — Connectors match on pin count and pitch

Even when the connector families agree, two connectors with mismatched `pin_count` or `pitch_mm` won't physically seat. Mismatches raise [`PinCountMismatchError`](https://github.com/raeq/wirebench/blob/main/src/framework/errors.py) or [`PitchMismatchError`](https://github.com/raeq/wirebench/blob/main/src/framework/errors.py).

**Why:** A 4-pin plug into a 5-pin receptacle leaves one pin unmated; a 2.54 mm plug into a 2.00 mm receptacle lands the pins between contacts, not on them. Either way the connection is physically incomplete. As with Rule 6, the bench equivalent is a parts order that arrives, doesn't fit, and goes back.

**First caught at:** [`fan_cooling/` — *A wasted parts order — wrong-pin-count power plug*](../demos/fan_cooling/README.md#a-wasted-parts-order--wrong-pin-count-power-plug).
**First caught at:** [`fan_cooling/` — *A wasted parts order — wrong-pin-count power plug*](https://github.com/raeq/wirebench/blob/main/demos/fan_cooling/README.md#a-wasted-parts-order--wrong-pin-count-power-plug).

## Rule 8 — Refdes uniqueness per circuit

Expand All @@ -87,7 +87,7 @@ Some logical states are valid wirings whose evaluation produces undefined or des

**Why:** Real silicon refuses to enter these states — or worse, enters them once and lets the smoke out. The SR latch with both inputs high is the canonical example: each NOR gate would force the other to zero, so neither output settles, and the chip behaviour is undefined. The framework catches this at `evaluate()` time and names the offending state with a suggested fix (drive the two inputs from mutually-exclusive sources, or use a latch type with no forbidden state).

**First caught at:** [`water_alarm/` — *A locked-up latch*](../demos/water_alarm/README.md#a-locked-up-latch) (S=R=1 on the NOR latch).
**First caught at:** [`water_alarm/` — *A locked-up latch*](https://github.com/raeq/wirebench/blob/main/demos/water_alarm/README.md#a-locked-up-latch) (S=R=1 on the NOR latch).

## Rule 10 — `wire()` doesn't merge pre-existing nets

Expand Down
48 changes: 29 additions & 19 deletions tests/docs/test_rules_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,25 +129,32 @@ def _readme_anchors(readme_path: Path) -> set[str]:


def test_every_demo_cross_link_resolves(rules_doc_text: str) -> None:
"""`../demos/<slug>/README.md#<anchor>` links must resolve: the
README file exists and the anchor matches a heading in it. Plain
`../demos/<slug>/` directory links (no anchor) must point at a
real demo directory."""
# Anchor-bearing README links.
"""Absolute GitHub URLs of the form
`https://github.com/raeq/wirebench/blob/main/demos/<slug>/README.md#<anchor>`
must resolve: the README file exists and the anchor matches a
heading in it.

mkdocs strict mode rejects relative links to files outside the
`docs/` tree (the README lives under `demos/`, which isn't part
of the published site), so the demo cross-links use the absolute
GitHub URL — same pattern this doc uses for the
`src/framework/errors.py` source link.
"""
readme_links = re.findall(
r'\(\.\./demos/([a-z0-9_]+)/README\.md#([a-z0-9-]+)\)',
r'https://github\.com/raeq/wirebench/blob/main/demos/'
r'([a-z0-9_]+)/README\.md#([a-z0-9-]+)',
rules_doc_text,
)
assert readme_links, (
"the-rules.md should link to demo README sections (the "
"*what this design is protected from* near-miss snippets), "
"not just demo folders — that's what gives rule entries their "
"first-caught traceability."
"*what this design is protected from* near-miss snippets) "
"that's what gives rule entries their first-caught "
"traceability."
)
for slug, anchor in readme_links:
readme = REPO_ROOT / 'demos' / slug / 'README.md'
assert readme.is_file(), (
f"Doc links to ../demos/{slug}/README.md but {readme} "
f"Doc links to demos/{slug}/README.md but {readme} "
f"doesn't exist"
)
anchors = _readme_anchors(readme)
Expand All @@ -156,15 +163,18 @@ def test_every_demo_cross_link_resolves(rules_doc_text: str) -> None:
f"that README produces that GitHub anchor. Available "
f"anchors: {sorted(anchors)}"
)
# Plain directory links (no fragment) — also verify the dirs exist.
plain_dir_links = set(re.findall(
r'\(\.\./demos/([a-z0-9_]+)/\)', rules_doc_text,
))
for slug in plain_dir_links:
path = REPO_ROOT / 'demos' / slug
assert path.is_dir(), (
f"Doc links to ../demos/{slug}/ but {path} doesn't exist"
)


def test_no_relative_demo_links_remain(rules_doc_text: str) -> None:
"""Relative links into `../demos/` would be rejected by mkdocs
strict mode (the demo READMEs aren't part of the mkdocs site
tree). Pin the absolute-URL convention so a future edit doesn't
silently re-introduce the failure."""
relative = re.findall(r'\]\(\.\./demos/[^)]*\)', rules_doc_text)
assert not relative, (
f"Relative `../demos/...` links break mkdocs strict mode — "
f"use absolute GitHub URLs. Offenders: {relative}"
)


def test_doc_cross_link_to_errors_source_resolves(
Expand Down