Emitter

Solved rig in, files out. Pure rendering under a strong contract: it decides nothing and cannot fail on an assembly the analyzer accepted. Its output is sorted by stable keys throughout, so declaration order in a rig file never reaches a rendered artifact.

rigc.emitter

The emitter: a Solved rig plus its Rig/ConnectorType inputs projected into concrete artifacts, split by visibility:

MCU-visible-static -> .overlay (joins the unchanged Zephyr build) human-realized -> config sheet (markdown) runtime-discoverable -> expectations (YAML stub)

Pure rendering under a strong contract: never decides anything, never fails on an analyzer-accepted rig. Deterministic: all output is sorted by stable keys, independent of rig-file declaration order.

Split into overlay.py (nexus synthesis, I2C/SPI scopes, collections, plain groups, controllers, the device-node renderer), sheet.py (config-sheet.md, including the params table’s token resolution), expectations.py, and this module (composing emit() and the ONE writer). context.py renders context.cmake as its own value function, kept out of emit() so rig artifacts and build glue stay separate concerns; cli.py calls it alongside, never inside, emit().

Artifacts are `{filename: bytes}`, explicit UTF-8: config-sheet.md carries non-ASCII punctuation (arrows, em dashes), so the encoding is a real decision, not a formality. The sub-renderers return str (readable in unit tests); this module is the one place that encodes them into the bytes-valued mapping cli.py hands to write().

Reads `solved.wires`, never `rig.wires`: the loader’s rig.wires carries the RAW via <name> route string; the analyzer’s wire check returns NEW Wire values with the route resolved to a connector-type position INDEX, held only on Solved.wires. Every renderer below takes solved and never reaches back through rig.wires.

rigc.emitter.banner

The provenance banner every emitted artifact carries.

Its own module for one structural reason: overlay.py, sheet.py and expectations.py all need it, and the emitter package imports all three. Holding it in emitter/__init__.py therefore forced the package to assign the banner and only THEN import its own submodules – a genuine import cycle (pylint R0401) that also put five imports below module level (ruff E402). A leaf module both the package and its submodules can import kills both, and leaves one definition, which was always the point: three comment syntaxes carry this text (/* */ in the overlay and the includes file, <!– –> in the config sheet, # in expectations.yml) and none of them may drift from the others.

rigc.emitter.overlay

rig-gen.overlay and rig-gen-includes.dtsi’s payload: the device-tree projection of a Solved rig – nexus synthesis, I2C scopes + mux nesting, SPI/cs-gpios, collections, plain groups, controllers, the device-node renderer.

Label policy: generated label = <instance>_<shield-local label>, e.g. logger_a_dl_rtc.

Per-instance parameters: a rig-assigned params: value is emitted VERBATIM – the raw token text, never resolved here – so rig-gen.overlay stays readable (zephyr,code = <INPUT_KEY_1>;, not a bare number). Resolving those tokens is sheet.py’s concern (the config sheet’s human-facing display value), not this module’s.

rigc.emitter.sheet

config-sheet.md – the physical configuration sheet: the ONE place a symbol’s resolved value is shown to a human; emission itself never resolves anything (overlay.py emits params verbatim), so without this table a rig-assigned INPUT_KEY_1 would mean nothing to a reader who has not memorized the header.

Reads `solved.wires`, never `rig.wires`: the Wires section is the one place in this module a wire’s raw Rig data would silently diverge from what got resolved – solved.wires carries the route already resolved to a connector-type position index.

Slot qualifier rendered only for a plural shield: a single-plug instance’s Socket-assignment row is a plain socket name; a plural instance gets one row per slot, its socket cell spelled <slot>: <ref-or-label>.

rigc.emitter.expectations

expectations.yml: a runtime-harness stub naming what must be observed on real hardware. Emitted for every accepted rig, but gated by no golden (test_emitted_corpus.py’s own docstring: “expectations.yml is deliberately excluded – it is emitted but never gated”).

Reads `solved.wires`, never `rig.wires` – same reasoning as sheet.py’s Wires section.

rigc.emitter.context

context.cmake – the build-glue handoff, not a rig artifact: a cmake fragment the rig build module (dts.cmake) include()s to learn what the rig instantiated. Kept out of emit() so rig artifacts and build glue stay separate concerns: this is a pure value function over (rig, deps); cli.py calls it alongside, never inside, emitter.emit().

rigc is the sole authority on what pass 1 actually read: deps is the UNION of every real source-tree file this run’s loader/registry/board calls touched (rig.yml, its content file, .shield templates and their cpp includes, connector bindings, index headers, the board .dts) – cmake/modules/dts.cmake appends RIG_DEPENDS (sorted, absolute) to CMAKE_CONFIGURE_DEPENDS on top of its own static registrations.