From 8055a3725ff134f86d7a45cbab9db60053a7c185 Mon Sep 17 00:00:00 2001 From: subzero Date: Wed, 20 May 2026 20:34:21 +0200 Subject: [PATCH] docs: fix mkdocs strict-mode failure on rules-doc demo deep-links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Phase 2b.2 follow-up review changed *First caught at* links from demo-folder roots to README#anchor deep-links so the rule entries point at the specific near-miss snippet. The new links used relative paths (`../demos//README.md#anchor`), but mkdocs strict mode rejects relative links to files outside the `docs/` tree (the README lives under `demos/`, which isn't a mkdocs site file) — the docs deploy aborts with: WARNING - Doc file 'the-rules.md' contains a link '../demos/hello_led/README.md#a-shorted-supply', but the target '../demos/hello_led/README.md' is not found among documentation files. Aborted with 8 warnings in strict mode! Switch the eight demo deep-links to absolute GitHub URLs — same pattern this doc already uses for the `src/framework/errors.py` source link. GitHub renders the same anchor algorithm for in-repo READMEs, so the deep-links resolve on both the docs site and on GitHub itself. Test coverage: - `test_every_demo_cross_link_resolves` now matches the absolute URL shape and still verifies the README + heading anchor exist on disk. - `test_no_relative_demo_links_remain` pins the convention so a future edit can't silently re-introduce the strict-mode failure. mkdocs build --strict: clean. Suite: 4796 passed. --- docs/the-rules.md | 16 ++++++------ tests/docs/test_rules_doc.py | 48 ++++++++++++++++++++++-------------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/docs/the-rules.md b/docs/the-rules.md index d057f38..51d1f4b 100644 --- a/docs/the-rules.md +++ b/docs/the-rules.md @@ -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 @@ -27,7 +27,7 @@ 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 @@ -35,7 +35,7 @@ Some pins are declared `mandatory=True` because the part doesn't function withou **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 @@ -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 @@ -55,7 +55,7 @@ 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 @@ -63,7 +63,7 @@ Every connector class declares its physical mate via `MATES_WITH`. Calling `mate **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 @@ -71,7 +71,7 @@ Even when the connector families agree, two connectors with mismatched `pin_coun **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 @@ -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 diff --git a/tests/docs/test_rules_doc.py b/tests/docs/test_rules_doc.py index f5eb4cb..e932a06 100644 --- a/tests/docs/test_rules_doc.py +++ b/tests/docs/test_rules_doc.py @@ -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//README.md#` links must resolve: the - README file exists and the anchor matches a heading in it. Plain - `../demos//` 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//README.md#` + 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) @@ -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(