Skip to content

feat: capture the whole wire surface, not just declarations - #3

Merged
cayossarian merged 2 commits into
mainfrom
feat/wire-capture
Aug 7, 2026
Merged

feat: capture the whole wire surface, not just declarations#3
cayossarian merged 2 commits into
mainfrom
feat/wire-capture

Conversation

@cayossarian

Copy link
Copy Markdown
Member

golden_tree.json holds $description documents, because conformance asks what a device declares. That cannot exercise a consumer: a parser fed only descriptions can be asked whether it understands the shape of a panel, never whether it builds the right snapshot from one — which is the part that reaches a user.

golden_wire.json holds the retained surface instead: descriptions, $state, and every property value, keyed as a consumer receives them.

37 devices, 37 described, 494 property values

span-panel-api-schema-1 vendors it and drives its parser end to end from it. That is why it is published as an artifact rather than kept internal.

The capture goes through production wiring

start_clone grows an optional publisher parameter, and the capture uses it. Reassembling the emitter inside the script would make it a second copy of that wiring, free to drift — and a capture taken through different wiring than a real panel uses proves less than it appears to.

MqttPublisher was already a Protocol and CloneRuntime.mqtt already held that type, so this is the existing seam being used rather than a new one. No broker is needed.

Shape is asserted; values are not

noise_factor perturbs power and current and the clock advances, so values differ every run. The test compares which devices and which topics — a property added, removed or renamed fails it, a different wattage does not.

That is the opposite policy to golden_report.json, deliberately: a conformance profile must not drift silently, while a fixture of noisy floats held to byte equality produces only failures nobody can act on.

Mutation-checked — injecting one phantom topic into the committed fixture fails the staleness guard by device id.

Placement

test_boundary.py caught my first attempt, correctly. The capture lives in emitter_adapter, not conformance: taking it means running this simulator, while the conformance rules describe any eBus publisher's output. The test sits in tests/conformance/ because the fixture it guards does.

Verified

Check Result
Test suite 432 passed (5 new)
pre-commit run --all-files all hooks pass
mypy --strict on the changed runtime clean
Staleness guard fails on an injected topic, passes on restore
Wire and tree captures same 37 devices

Known gap, unchanged by this PR

grid/islanding-state is still absent, because no config can produce a MID. The emitter supports one fully — wire/profiles/mid.json declares the grid node, bag_builder resolves ("mid", "grid/islanding-state"), snapshot carries islanding_state, and Emitter iterates of_class("mid") — but spec_generator.build_manifest() never creates a MID DeviceInstance. So the gap is one missing instance in the manifest builder, not a config or capability gap.

It is the single property span-panel-api-schema-1 reads that this simulator does not exercise, and it is the one whose mapping was corrected most recently (grid_state reads islanding-state, not grid-state). Worth closing, but it changes what the simulator publishes and would require recapturing both fixtures, so it belongs in its own PR.

golden_tree.json holds $description documents, because conformance asks what a
device declares. That cannot exercise a consumer: a parser fed only descriptions
can be asked whether it understands the shape of a panel, never whether it builds
the right snapshot from one, which is the part that reaches a user.

golden_wire.json holds the retained surface instead -- descriptions, $state, and
every property value, keyed the way a consumer receives them. 37 devices, 494
values. span-panel-api-schema-1 vendors it and drives its parser end to end from
it, which is why it is an artifact rather than something internal.

start_clone grows an optional publisher parameter, and the capture goes through
it. Reassembling the emitter inside the script would make it a second copy of the
production wiring, free to drift, and a capture taken through different wiring
than a real panel uses proves less than it appears to. MqttPublisher was already
a Protocol and CloneRuntime already held that type, so this is the seam being
used rather than a new one.

Values are not reproducible -- noise_factor perturbs power and current, and the
clock advances -- so the test compares shape: which devices, which topics. A
property added, removed or renamed fails it; a different wattage does not. That
is the opposite policy to golden_report.json on purpose. A conformance profile
must not drift silently, while a fixture of noisy floats held to byte equality
would only produce failures nobody can act on.

The capture lives in emitter_adapter rather than conformance: taking it means
running this simulator, while the conformance rules describe any eBus publisher's
output. test_boundary.py enforces that and caught the first placement.
check-spec-provenance.py cloned the specification on every run unless given
--spec. It now defaults that flag from EBUS_SPEC_DIR, so a path set once in .env
saves a network round trip per run.

Purely an optimisation. Cloning still works and needs no setup, and a variable
pointing at a directory that has since moved falls back to cloning rather than
failing -- a stale path is the same situation as an unset one, and failing on it
would punish someone for a line they wrote months ago.

Same variable name as span-panel-api, whose schema_1 suite byte-compares its own
vendored catalogs against a checkout. One name for one thing, so a single line in
a shell serves both repositories.
@cayossarian
cayossarian merged commit 397758e into main Aug 7, 2026
3 checks passed
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.

1 participant