feat(plc): factorylm.machine-snapshot.v1 producer over canonical tags (MIRA PRD #3048, PR 2) - #198
Conversation
…tags (PRD #3048 PR 2) Builds the versioned, read-only machine-snapshot envelope from the canonical conv_simple tags that canonical_tags_from_snapshot() (#188, included here rebased onto main) projects out of a Micro820 TagSnapshot. - source_system is plc_bridge (MIRA ingest rejects anything else); the FactoryLM identity rides in provenance.producer per the PRD's 2026-08-02 amendment. - machine_state/active_conditions derived deterministically; comm loss beats fault beats running — a bridge that cannot talk to the PLC does not claim the motor is running. - Quality only ever downgrades: comms lost -> measurements uncertain, while comm_ok/fault_code stay good (they are the bridge's own state). - tenant_id/snapshot_id/captured_at are never defaulted or invented; captured_at comes from the snapshot's own timestamp. - Pure data reshaping: no Modbus, sockets, or network imports — proven by test. Observation-only enforced by validate_envelope, which rejects command/write-shaped fields at any depth. Vendors the SHARED contract fixtures from MIRA PR #3052 verbatim into contracts/machine_snapshot/ — both repos test the exact same payloads. validate_envelope passes the golden fixture untouched, fails each of the four invalid fixtures for its documented reason, and the producer's output is asserted shape-identical to the golden payload. 180 unit tests pass: 151 pre-existing + 5 from #188 + 24 new. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n
- #199: shared fixtures carry all seven conv_simple.* canonical tags (adds conv_simple.sort_divert_active); golden-shape test now compares tag path sets, not just tag-entry key shape - #200: comms-loss state renamed comm_lost -> comm_down to match MIRA's current-state vocabulary - #201: validate_envelope now enforces source_system=plc_bridge and canonical tag paths; unknown quality is accepted (consumer downgrades to uncertain, never good) instead of rejected - #202: factorylm_plc/__init__ hardware exports are lazy (PEP 562) so importing machine_snapshot pulls no pymodbus; proven by a fresh-interpreter import test - #203: active E-stop returns "estopped" (comm_down still dominant); an E-stopped machine can never surface as merely "running" 188 unit tests pass (baseline was 180). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n
|
Repair pass complete for the adversarial review (commit 2926e38):
Note: landed as one atomic commit rather than the suggested two — the #200/#203 changes interleave in the same function/docstring, and the #202 import-purity test would fail at any intermediate commit without the lazy Tests (local, since only No remote publishing, no relay integration, no plant writes. Heads-up for the MIRA side: MIRA |
…r read P1 from the post-repair review: height_sensor_mm and sort_divert_active default to 0/False in canonical_tags_from_snapshot when the snapshot's io dict lacks them — and ModbusTagSource.tick() never populates those keys, because the bench Micro820 map has no height-sensor or sort-divert I/O. The envelope then claimed quality=good for values that were deterministic defaults, not observations. The seven-tag contract shape is kept; unsourced io-backed tags now emit quality=uncertain (downgrade-only rule — a value the bridge never read is never good). Wiring fake signals into the reader was rejected: that would invent plant data the hardware map does not carry. - modbus_tag_source: IO_SOURCED_CANONICAL_TAGS + unsourced_canonical_tags() - machine_snapshot: _tag_quality downgrades unsourced tags first - tests: unsourced-vs-read quality cases + fake-pymodbus-client tick() end-to-end proof that the REAL reader path emits uncertain for both 191 unit tests pass (was 188). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n
|
P1 from the post-repair review fixed (commit 19b44e2): Finding: Fix taken: kept the seven-tag contract shape, but unsourced io-backed tags now emit
P2 sequencing executed: #206 merged first, so this push runs the real |
|
Reviewed the full diff. The producer is good work — it honors every PRD amendment ( Two things before merge. 1.
|
|
Follow-up from MIRA #3060 (just opened): I added This PR should vendor that file and add the equivalent assertion, or the guard only protects one side — and a one-sided edit is precisely the failure mode, since each repo would keep passing against its own copy. The six files are byte-identical between the two repos as of this review (I verified sha256 across all of them), so vendoring |
… E-stop across a dead link (#207) (#209) Three review items from MIRA PRD #3048 that merged with #198 unaddressed. All three feed MIRA's technician-visible live evidence. 1. Communication health was BEING error code 5, in two places independently (machine_state_from_snapshot, and canonical_tags_from_snapshot deriving conv_simple.comm_ok from `int(error_code) != 5`). Renumbering ERROR_CODES would silently change what machine_state means. TagSnapshot now carries an optional `comm_ok` link indicator, and comm_ok_from() is THE one definition both call sites use -- authoritative when the source supplies it, falling back to the documented error-code proxy only when it does not (which is all the Micro820 bridge exposes today). 2. active_conditions asserted e_stop_active unconditionally, then returned comm_down -- claiming a CURRENT E-stop read across the same dead link that _tag_quality already declares unreliable. That is the dangerous direction: an E-stop reported as engaged when it may have been released since comms dropped can lead a technician to believe a machine is safed when it isn't. It is now reported as e_stop_active_last_known so the consumer can render the uncertainty. A pre-existing test asserted the old behaviour while its own docstring said the opposite ("the bridge cannot vouch for E-stop either") -- updated to match its stated reasoning. 3. The forbidden-field scan was a SUBSTRING denylist, so controller_model, controller_serial and control_panel_id were all rejected as command fields. Live rather than theoretical since MIRA #3059 began writing snapshot fields into per-tag metadata. Now matched on whole _-separated segments, with bare `control` dropped (ambiguous -- control_panel_id is provenance) and the actual control-word shapes named explicitly. A real command/write/actuate/ setpoint_write field is still rejected at any depth. 195 passed on main -> 204 passed here (+9), zero regressions. Co-authored-by: CharlieNode <charlienode@CharlieNodes-Mac-mini.local>
PR 2 of MIRA PRD #3048 — FactoryLM canonical-source correctness. Companion to #197 (the #161 bug fix) and to MIRA-side #3052 (BRAVO's consumer adapter).
What this contains
1. #188's canonical-tag mapping, rebased onto current
main(cherry-pick, commit preserved verbatim with original authorship — its 5 tests pass unchanged). #188 itself can be closed when this merges; the PRD forbade force-pushing that branch.2.
machine_snapshot.py(new) — produces thefactorylm.machine-snapshot.v1envelope fromcanonical_tags_from_snapshot()output:source_system: "plc_bridge"— notfactorylm-plc-modbus, which MIRA's ingest rejects (VALID_SOURCE_SYSTEMS); the FactoryLM identity rides inprovenance.producerper the PRD's 2026-08-02 amendmentmachine_state/active_conditions; comm loss beats fault beats running — a bridge that cannot talk to the PLC does not claim the motor is runninguncertain;comm_ok/fault_codestaygood(they are the bridge's own directly-known state)tenant_id/snapshot_id/captured_atnever defaulted or invented —captured_atcomes from the snapshot's own timestamp, no clocks anywherevalidate_envelope()rejects command/write-shaped fields at any depth3. Shared contract fixtures vendored verbatim from MIRA #3052 into
contracts/machine_snapshot/— both repos now test the exact same payloads. The golden fixture passesvalidate_envelopeuntouched; each of the four invalid fixtures fails for its documented reason; the producer's output is asserted shape-identical to the golden payload (same key sets, same vocabularies).PRD PR-2 acceptance, mapped
test_healthy_envelope_is_valid_and_complete(all 7conv_simple.*)fault_code+comm_ok=falsetest_comms_lost_preserves_fault_code_and_comm_ok_falsetest_deterministic+ no-clock constructionTestSharedFixtureCompatibility(vendored fixtures, shape assert)test_module_is_pure_data_reshaping+ observation-only validator180 unit tests pass (151 pre-existing + 5 from #188 + 24 new), run on the PLC laptop.
Slice coordination
Claimed on MIRA #3048: PLC laptop (
LAPTOP-0KA3C70H), sessionf985f0fe. Depends on nothing; #197 (the #161 fix) is independent and both can merge in either order. If the fixtures change in MIRA #3052 review, re-synccontracts/machine_snapshot/here and re-runtests/unit/test_machine_snapshot.py.🤖 Generated with Claude Code
https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n