Skip to content

docs: cumulative-rules narrative (Phase 2b.2)#37

Merged
raeq merged 2 commits into
mainfrom
phase-2b-2-rules-narrative
May 20, 2026
Merged

docs: cumulative-rules narrative (Phase 2b.2)#37
raeq merged 2 commits into
mainfrom
phase-2b-2-rules-narrative

Conversation

@raeq

@raeq raeq commented May 20, 2026

Copy link
Copy Markdown
Owner

Summary

Third task of .plans/phase-2b-spec.mdthe rules feel arbitrary if you encounter them one at a time without the cumulative narrative; they feel principled if you can see the whole set on one page with the physical justification for each.

Adds docs/the-rules.md — a five-minute single-page index of every user-facing rule the framework enforces. Per entry: the rule itself, the physical-world referent for why, the exception class that fires (link to framework/errors.py), and the demo where users first see the rule caught.

Twelve rules covered:

  1. One driver per logical net → ShortCircuitError (first caught: hello_led/)
  2. Every BIDIR-only net needs a driver → FloatingNetError (first caught: hello_led/)
  3. Mandatory pins must be connected → UnconnectedPinError (first caught: 5v_rail_power/)
  4. Signal types stay matched → SignalTypeMismatchError (first caught: li_ion_fuel_gauge/)
  5. Ground domains stay isolated → DomainCrossingError (first caught: isolated_rs232/)
  6. Connectors mate only with their declared partner → IncompatibleMateError (first caught: fan_cooling/)
  7. Pin count + pitch match → PinCountMismatchError / PitchMismatchError (first caught: fan_cooling/)
  8. Refdes uniqueness per circuit → RefdesError
  9. Forbidden states stay forbidden → ForbiddenStateError (first caught: water_alarm/)
  10. wire() doesn't merge pre-existing nets → NodeMergeError
  11. Empty wires aren't wires → EmptyWireError
  12. A wired chip can't be called standalone → WiredChipCallError

Cross-references

  • learning-path.md gains a First catches column on the demo table naming the rule numbers each demo first surfaces. A closing paragraph notes that framework-internal refusals (Rules 8, 10, 11, 12) don't anchor to a single demo.
  • Each rule entry in the-rules.md links to the first-caught demo, so the cumulative property is visible: by the time you've worked through hello_led/ + water_alarm/ + 5v_rail_power/ + digital_thermometer/ + fan_cooling/ + isolated_rs232/ + li_ion_fuel_gauge/, you've seen the framework catch eight of the twelve rules in real demos.

Files

  • docs/the-rules.md — new, the cumulative-rules narrative.
  • docs/learning-path.md — adds First catches column + cross-reference paragraph; also fixes a pre-existing fenced-code-language lint warning along the way.
  • mkdocs.yml — adds The rules: the-rules.md to nav between Design principles (the abstract) and Learning path (the graduated exposure).
  • tests/docs/test_rules_doc.py — 33 new tests pinning consistency: every curated user-facing rule exception is named in the doc; every XxxError reference resolves to a real class (catches rename typos); every ../demos/<slug>/ link resolves to an actual directory; mkdocs nav orders design-principles → the-rules → learning-path.

Test plan

  • uv run pytest — 4795 passed (33 new), 16 skipped
  • uv run mypy src/ demos/ — clean
  • Markdown lint clean on touched docs (table alignment + fenced-code-language)
  • Manual review: cross-links resolve, tone reads as teacher rather than gatekeeper

Add `docs/the-rules.md` — a single page indexing every user-facing
rule the framework enforces.  Per entry: the rule, the physical
justification, the exception class that fires when violated (link to
`framework/errors.py`), and the demo where users first see the rule
caught.  Twelve rules in five-minute reading length.

Reviewer feedback addressed: "the rules feel arbitrary if you
encounter them one at a time without the cumulative narrative; they
feel principled if you can see the whole set on one page with the
physical justification for each."  The rules doc sits between
`design-principles.md` (the abstract) and `learning-path.md` (the
graduated exposure) in the site navigation.

Cross-references:

- `learning-path.md` gains a *First catches* column on the demo table
  naming the rule numbers each demo first surfaces; a closing
  paragraph notes that framework-internal refusals (Rules 8, 10, 11,
  12) don't anchor to a single demo.
- `the-rules.md` links each rule to its first-caught demo.

`tests/docs/test_rules_doc.py` keeps the doc in sync as the exception
hierarchy evolves: every curated user-facing rule exception is named
in the doc, every `XxxError` reference in the doc resolves to a real
class (catches rename typos), every `../demos/<slug>/` link resolves
to an actual directory, and the mkdocs nav orders design-principles
→ the-rules → learning-path.

Suite: 4795 passed (33 new), mypy clean.
Copilot AI review requested due to automatic review settings May 20, 2026 17:55

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

Adds a new “cumulative rules” documentation page and accompanying consistency tests so the user-facing framework rules, their exception classes, and cross-links stay synchronized as the exception hierarchy and demo set evolve.

Changes:

  • Add docs/the-rules.md as a single-page index of user-facing enforcement rules (with “why”, exception class, and first-caught demo).
  • Update docs/learning-path.md to add a “First catches” column and cross-reference the-rules.md; fix a fenced-code language tag.
  • Add tests/docs/test_rules_doc.py plus mkdocs navigation updates to keep docs, exceptions, demo links, and nav ordering consistent.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/docs/test_rules_doc.py Adds tests to ensure rule exceptions and doc links are consistent and mkdocs nav includes/ आदेशers the new page.
tests/docs/__init__.py Establishes tests/docs as a package (empty init).
mkdocs.yml Inserts “Design principles” and the new “The rules” page into the docs nav in the intended order.
docs/the-rules.md Introduces the single-page rules index with links to exception classes and demos.
docs/learning-path.md Adds “First catches” column and cross-references to the new rules page; fixes code-fence language.

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

Comment thread docs/the-rules.md Outdated
- **What fires** — the exception class wirebench raises ([source](https://github.com/raeq/wirebench/blob/main/src/framework/errors.py)).
- **First caught at** — the demo where you'll meet this rule for the first time if you walk the [learning path](learning-path.md) top to bottom.

By the time you've worked through `hello_led/` + `water_alarm/` + `5v_rail_power/` + `digital_thermometer/` + `fan_cooling/` + `isolated_rs232/` + `li_ion_fuel_gauge/`, you've seen the framework catch every rule on this page in a real demo. The rules aren't aspirational — they're the load-bearing checks that keep "the code matches the breadboard" honest.
Comment thread docs/the-rules.md Outdated

A `wire()` whose ports are already on two distinct nodes would silently fuse independent design intent into one net. The framework refuses and raises [`NodeMergeError`](https://github.com/raeq/wirebench/blob/main/src/framework/errors.py).

**Why:** Mergeing nets at construction time is a footgun for boards being composed from sub-circuits — a wire intended to extend one net could accidentally connect it to a parallel net the author hadn't realised was nearby. The framework requires the join to be explicit: refactor the code so the two nets are constructed together, or use a `Pin` / connector mate to bridge them deliberately.
Comment thread docs/the-rules.md Outdated

**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:** [`5v_rail_power/`](../demos/5v_rail_power/) — the *floating regulator input* near-miss.
Comment thread docs/learning-path.md Outdated
Comment on lines +24 to +40
| 2 | [`penfold_light_switch/`](../demos/penfold_light_switch/) | First sensor circuit. LDR + comparator + transistor switch. Penfold BP107 P3.| — |
| 3 | [`water_alarm/`](../demos/water_alarm/) | Composing chips, wiring rails, latching logic. Four chips, two LEDs. | [Rule 9](the-rules.md) |
| 4 | [`penfold_reaction_game/`](../demos/penfold_reaction_game/) | Sequential digital — counter ring + button-stops-clock topology. Penfold P22.| — |
| 5 | [`dice/`](../demos/dice/) | Classic 555 + 4017 + diode-OR matrix. Recognisable hobbyist staple. | — |
| 6 | [`digital_thermometer/`](../demos/digital_thermometer/) | First MCU project. ATmega328P + DHT11 + 7-seg display. Firmware-as-cell. | [Rule 3](the-rules.md) |
| 7 | [`penfold_one_second_timer/`](../demos/penfold_one_second_timer/) | Op-amp relaxation oscillator with hysteresis. Penfold BP107 P8. | — |
| 8 | [`penfold_metronome/`](../demos/penfold_metronome/) | NE555 astable + speaker — the other classical astable. Penfold BP107 P9. | — |
| 9 | [`penfold_warbling_doorbuzzer/`](../demos/penfold_warbling_doorbuzzer/)| Oscillator composition — slow gates fast. Penfold BP107 P16. | — |
| 10 | [`doorbell_protector/`](../demos/doorbell_protector/) | Two-555 monostable with transistor switching and a relay. | — |
| 11 | [`fan_cooling/`](../demos/fan_cooling/) | First `Board` demo. TMP302 + MOSFET-switched fan. Connectors that mate. | [Rules 6, 7](the-rules.md) |
| 12 | [`backup_power/`](../demos/backup_power/) | TI Designs TIDA-03031. Three-stage power architecture (eFuse + boost + buck).| — |
| 13 | [`water_alarm_split/`](../demos/water_alarm_split/) | Same circuit as #3 but split across two boards via `mate()`. HAT pattern. | — |
| 14 | [`bldc_motor/`](../demos/bldc_motor/) | ATmega328P + DRV8313 + Hall sensors. Three-phase commutation. | — |
| 15 | [`isolated_rs232/`](../demos/isolated_rs232/) | TIDA-01230. Cross-domain isolation — first demo to exercise `GroundDomain`. | [Rule 5](the-rules.md) |
| 16 | [`li_ion_fuel_gauge/`](../demos/li_ion_fuel_gauge/) | TIDA-00594. BQ27546-G1 fuel gauge with sense resistor + thermistor. | [Rule 4](the-rules.md) |
| 17 | [`penfold_fuzz_unit/`](../demos/penfold_fuzz_unit/) | Audio domain — op-amp + clipping diodes. Guitar fuzz pedal. Penfold P30. | — |
| 18 | [`penfold_crystal_set/`](../demos/penfold_crystal_set/) | Passive-only RF — no Rail, no battery. Boundary case. Penfold BP107 P27. | — |
…ngthen test

Reviewer feedback addressed:

**Accuracy.** Rule 3 (`UnconnectedPinError`) and Rule 6
(`IncompatibleMateError`) listed demos that don't actually catch
those exceptions in their READMEs.  Surveyed every demo README for
the exceptions it raises and re-mapped each rule to the truly-first
demo in learning-path order:

  - Rule 3 `UnconnectedPinError` → `penfold_light_switch/` (the
    "floating LDR" near-miss raises UnconnectedPinError; the LDR's
    terminals are mandatory), not `5v_rail_power/` or
    `digital_thermometer/`.
  - Rule 4 `SignalTypeMismatchError` → `penfold_light_switch/` (the
    "mismatched comparator input domain" near-miss), not
    `li_ion_fuel_gauge/`.
  - Rule 6 `IncompatibleMateError` → `water_alarm_split/` (the
    "wrong connector family" near-miss), not `fan_cooling/`.

Updated `learning-path.md`'s *First catches* column to match.

**Traceability.** Per the spec — "link to the demo's *what this
design is protected from* sidebar" — every `First caught at:` entry
now deep-links to the specific README#anchor of the near-miss
section, not just the demo folder.  Anchors generated from the
section heading text using GitHub's slug algorithm.

**Test coverage.** Strengthened `test_every_demo_cross_link_resolves`
to compute each README's GitHub-style heading anchors and verify
every `#anchor` in `the-rules.md` points at a real heading.  Catches
typos in the slug and renames of section headings on the demo side.

**Intro overclaim.** Softened the cumulative-progress claim — the
intro previously asserted you'd see "every rule on this page" caught
in real demos by working through a subset of demos, but Rules 8, 10,
11, 12 are framework-internal refusals with no demo anchor.  Now
explicitly: "the demo-anchored rules (Rules 1–7, 9)".

**Typo.** "Mergeing" → "Merging" on Rule 10's *Why:* line.

Suite: 4795 passed, mypy clean.
@raeq

raeq commented May 20, 2026

Copy link
Copy Markdown
Owner Author

Thanks — multiple substantive issues. All fixed in af8808d.

Inaccurate rule→demo mappings. Surveyed every demo README for the exceptions it actually raises and re-mapped each rule to the truly-first demo in learning-path order:

  • Rule 3 (UnconnectedPinError) → penfold_light_switch/ (the "floating LDR" near-miss raises UnconnectedPinError because the LDR's terminals are mandatory). Previously 5v_rail_power/, which the doc grep showed raises FloatingNetError (Rule 2), not Rule 3.
  • Rule 4 (SignalTypeMismatchError) → penfold_light_switch/ ("mismatched comparator input domain"). Earlier than li_ion_fuel_gauge/.
  • Rule 6 (IncompatibleMateError) → water_alarm_split/ ("wrong connector family"). fan_cooling/'s near-miss is PinCountMismatchError (Rule 7), not 6.
  • Rule 7 (PinCountMismatchError) stays at fan_cooling/ (correct).

Updated learning-path.md's First catches column to match.

Demo links promised by the spec → README anchors, not folder roots. The spec asked for "link to the demo's what this design is protected from sidebar." Now every First caught at: entry deep-links to the specific README.md#anchor of the near-miss section, computed from the section heading via GitHub's slug algorithm.

Test was permissive — now strict. test_every_demo_cross_link_resolves parses each README's ## … / ### … headings, computes their GitHub anchors, and verifies every #anchor referenced in the rules doc points at a real heading. Catches typos in the slug and silently-renamed headings on the demo side.

Intro overclaim. Softened the cumulative-progress sentence — previously it implied you'd see every rule caught in real demos, but Rules 8, 10, 11, 12 are framework-internal refusals with no demo anchor. Now: "the demo-anchored rules (Rules 1–7, 9)".

Typo. "Mergeing" → "Merging" on Rule 10's Why: line.

Open questions:

  • Hard-link to GitHub main vs repository-relative for framework/errors.py? — Kept the GitHub main URLs. Repository-relative paths (../../src/framework/errors.py) don't work on the published mkdocs site (the site root doesn't include src/). A relative ../src/… would resolve in the GitHub-rendered README but not in the published docs. The mkdocs-material repo_url template variable would solve it cleanly but needs the macros plugin. I think pinning main is fine — if it becomes an issue we can revisit with the plugin.

Suite: 4795 passed (same count; 33 docs tests now exercise stronger invariants), mypy clean.

@raeq raeq merged commit af3dfe5 into main May 20, 2026
3 checks passed
@raeq raeq deleted the phase-2b-2-rules-narrative branch May 20, 2026 18:10
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