Skip to content

docs(prd): MIRA × FactoryLM Phase 1 — read-only machine evidence handoff - #3048

Merged
Mikecranesync merged 4 commits into
mainfrom
codex/mira-factorylm-machine-evidence-prd
Aug 2, 2026
Merged

docs(prd): MIRA × FactoryLM Phase 1 — read-only machine evidence handoff#3048
Mikecranesync merged 4 commits into
mainfrom
codex/mira-factorylm-machine-evidence-prd

Conversation

@Mikecranesync

Copy link
Copy Markdown
Owner

Adds the Phase-1 PRD for handing FactoryLM's canonical PLC snapshot to MIRA's existing audited technician context as read-only live evidence.

Docs-only (one file, +291). No code, no migration, no dependency — exempt from the VERSION bump per docs/versioning.md.

Premise verification (I checked every external claim before opening this)

Branch is current with origin/main (0 behind; merge-base cb49901a = #3041). Every referenced symbol and file resolves on origin/main:

PRD claim Verified
materialized_evidence/context_contract.pyLiveTag, LiveStateOverlay, live_overlay_from_machine_packet ✅ present
TechnicianContext.live: LiveStateOverlay | None = None ✅ present, and to_dict() serializes it
mira-bots/shared/technician_context.pybuild_turn_context, manifest_of ✅ present
mira-bots/shared/engine.py_build_prior_decisions_context, _context_manifest, _build_live_data_context ✅ 2 / 9 / 2 hits
mira-bots/shared/live_snapshot.py ✅ present
build_turn_context has no live parameter today ✅ confirmed — PR 1's premise is accurate
FactoryLM #188 (draft), #161 (open), #192 (open); MIRA #3046 (open, DIRTY), #3038 (draft), #2989 (open) ✅ all live and accurately described
/Users/charlienode/factorylm is dirty/user-owned ✅ confirmed — on feat/mission-control-demo-pipeline, ahead 2, modified files. Use a fresh worktree as the PRD says.

Preflight step 5/6 is now answered: an authorized ingress exists

The PRD says to stop after the contract/fixture PRs if no existing ingress can carry this payload. It can:

  • POST /api/v1/tags/ingestmira-relay/relay_server.py::tags_ingest
  • HMAC auth via _authenticate_http; the HMAC tenant is authoritative and a caller-supplied body tenant is only honored on the non-HMAC dev/bench path — exactly the "never default or infer tenant_id" rule the contract asks for
  • mira-relay/tag_ingest.py::ingest_batch, fail-closed against approved_tags

This is also mandatory, not merely available: .claude/rules/one-pipeline-ingest.md forbids any transport from forking its own normalizer/allowlist/persistence/batch-shape. So PR 3 is buildable, and a second relay would be a rule violation rather than a design choice.

Four gaps found against the real ingest contract

These are not blockers, but PR 3 cannot be built as literally specified without resolving them.

  1. source_system: "factorylm-plc-modbus" will be rejected. ingest_batch validates against VALID_SOURCE_SYSTEMS = {"ignition", "plc_bridge", "relay", "simulator"} and raises invalid_source_system otherwise. Reuse-first answer: send plc_bridge (semantically exact) and carry the FactoryLM identity in provenance — rather than widening the vocabulary at the single enforcement point.

  2. The PRD never mentions seeding approved_tags — without it every tag is dropped. The allowlist is fail-closed on normalize_tag_path(tag_path). All seven canonical tags need rows (conv_simple.vfd_speed_hzconv_simple_vfd_speed_hz, etc.) for the tenant + source_system, or ingest_batch returns not_allowlisted for all of them and the overlay is silently empty. Precedent exists: tools/seeds/approved_tags_conveyor.sql, gen_approved_tags_simulator.py, migration 035_approved_tags.sql. This is a missing work package.

  3. Envelope fields have no home in the canonical batch shape. build_ingest_batch carries {source_system, tags[], tenant_id?, source_connection_id?} and build_tag_entry carries {tag_path, value, value_type, quality, ts?, equipment_entity_id?, metadata?}. The envelope's machine_state, active_conditions, snapshot_id, captured_at, schema_version, provenance, proposed_uns_path have nowhere to go — yet LiveStateOverlay requires machine_state and active_conditions. They ride in metadata or the design needs an explicit decision. This is the most consequential unresolved question in the doc.

  4. The relay persists; it does not carry a request-scoped snapshot. ingest_batch writes tag_events + live_signal_cache. So PR 4 cannot "receive the snapshot inline at turn time" — it must read back current state at turn time. Worth pinning explicitly, since the current wording ("wire the accepted snapshot from the established ingress/state carrier") permits both readings.

One design note

_build_live_data_context is not relay-based — it HTTP-GETs mira-fault-detective /current_fault. So the PRD's duplicate-[LIVE EQUIPMENT STATUS] concern is real: these are two genuinely independent live paths, and PR 4's dedup requirement is load-bearing.

Also: #3041 added retrieval evidence via a separate augment_with_retrieval(ctx, chunks) rather than extending build_turn_context. PR 1 proposes extending build_turn_context instead. Matching the augment_with_* precedent would be a smaller diff and a consistent shape — worth a decision before PR 1 starts.

Scope

Doc only. No code was written and nothing was built against this PRD yet.

…ntract

Verified every claim against origin/main before any code was written. Four
corrections, each inlined and marked:

- source_system 'factorylm-plc-modbus' would have been rejected outright —
  ingest_batch validates VALID_SOURCE_SYSTEMS {ignition, plc_bridge, relay,
  simulator} (tag_ingest.py:59). Use plc_bridge; FactoryLM identity moves to
  provenance.producer.
- The envelope->canonical-batch mapping was unspecified, and machine_state /
  active_conditions had nowhere to go in build_tag_entry despite being required
  to construct a LiveStateOverlay. They now ride metadata.factorylm_snapshot,
  with the two quality vocabularies mapped explicitly (downgrade only).
- Seeding approved_tags is a PREREQUISITE of PR 3, not a follow-up. The
  allowlist is fail-closed, so without it a valid snapshot returns accepted=0
  and every acceptance test passes vacuously against an empty overlay.
- PR 4 reads state back at turn time. ingest_batch persists to tag_events +
  live_signal_cache; it never hands a request-scoped snapshot to the engine.

Also records that the preflight's 'stop if no ingress exists' contingency does
not fire (the ingress exists and is mandatory under the one-pipeline law), and
recommends the augment_with_* shape #3041 established over growing
build_turn_context's signature per evidence family.
@Mikecranesync
Mikecranesync merged commit 5691f0d into main Aug 2, 2026
23 checks passed
@Mikecranesync

Copy link
Copy Markdown
Owner Author

🔒 Slice claim — MIRA PR 1 (contract adapter + augment_with_live + fixtures)

If another session already has PR 1 in flight, ping here and I'll stand down. Otherwise I'm building it now.

@Mikecranesync

Copy link
Copy Markdown
Owner Author

🔒 Slice claim — PR 2 (FactoryLM canonical-source correctness)

If another session already has PR 2 in flight, ping here and I'll stand down. Otherwise building now.

@Mikecranesync

Copy link
Copy Markdown
Owner Author

PR 2 slice code-complete (PLC laptop, session f985f0fe)

@BRAVO/#3052: if the fixtures change in your review, ping here — factorylm re-syncs its vendored copy and re-runs test_machine_snapshot.py. Producer output is asserted shape-identical to your golden payload.

Not done (per PRD, not mine): remote publishing (PR 3, unclaimed — includes the fail-closed approved_tags seeding), PR 4, PR 5 (integration proof wants this bench; I can take it once 1–4 land). Both factorylm PRs await Mike's review/merge.

Mikecranesync added a commit that referenced this pull request Aug 2, 2026
…ter (PRD #3048, PR 1) (#3052)

First slice of the read-only machine-evidence handoff: the live-state evidence
family enters the context contract, after prior-decision (#3032) and retrieval
(#3041). Pure Python — no ingress, no infra, no plant writes.

- overlay_from_factorylm_snapshot(snapshot) -> (LiveStateOverlay | None,
  violations): validates the factorylm.machine-snapshot.v1 envelope and maps it
  to the MachineContextPacket dict live_overlay_from_machine_packet already
  consumes, then delegates — no duplicated LiveTag/freshness/render logic.
  Quality {good,bad,stale,uncertain} -> Freshness, always downgrading toward
  less confidence (unknown never becomes good/live); simulator source marks tags
  simulated. Read-only by construction (asserted: no pymodbus/pycomm3/socket/
  httpx/write).
- technician_context.augment_with_live(ctx, snapshot): folds the overlay into
  TechnicianContext.live on the SAME context (augment_with_* shape from #3041),
  re-validates, one manifest. Accepts an envelope dict or a prebuilt overlay.
  Fail-open.
- contracts/machine_snapshot/: shared cross-repo fixture (valid + 4 invalid +
  spec) — the compatibility boundary both repos test against.
- Exported overlay_from_factorylm_snapshot + FACTORYLM_SNAPSHOT_SCHEMA.

Flag-gated serving-path wiring is PR 4; this changes no engine behavior. Tests:
test_factorylm_snapshot_adapter.py (9) + test_technician_context_live.py (6).
Regressions green (context_contract, technician_context, ws1_engine_wiring,
retrieval).

Built by: Claude Code (Opus 4.8) on node BRAVO (FactoryLM-Bravo.local, user bravonode).

Co-authored-by: Mike Harper <bravonode@FactoryLM-Bravo.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Mikecranesync

Copy link
Copy Markdown
Owner Author

🔒 Slice claim — PR 2 repair pass (PLC laptop LAPTOP-0KA3C70H, fresh Claude Code Fable 5 session)

@Mikecranesync

Copy link
Copy Markdown
Owner Author

PR 2 repair pass complete (PLC laptop, per CLAUDE_PR2_REPAIR_BRIEF.md)

All seven adversarial-review findings repaired:

Merges remain Mike's call (#198 → close #188 on merge, per standing instruction). No relay publishing, no approved_tags seeding, no plant writes — PR 3 scope untouched.

Mikecranesync pushed a commit that referenced this pull request Aug 2, 2026
… ingress (PRD #3048, PR 3)

PR 1 (v3.240.0) landed the envelope -> TechnicianContext.live adapter; this lands
the transport that actually delivers a snapshot. No new endpoint exists or could:
the one-pipeline law forbids a transport from defining its own normalizer,
allowlist, persistence, batch shape, or enforcement path, and test_architecture
Contract 5 fails the build on a violation. So the work is decode ->
build_tag_entry -> build_ingest_batch -> ingest_batch, the shape
simlab/publishers.py::RelayIngestPublisher established.

- mira-relay/factorylm_snapshot.py: snapshot_to_ingest_batch() (pure decode) +
  FactoryLMSnapshotPublisher (HMAC-signed POST to POST /api/v1/tags/ingest).
  Snapshot-scoped fields with no column of their own ride per-tag
  metadata.factorylm_snapshot; machine_state and active_conditions are REQUIRED
  to build a LiveStateOverlay, so losing them silently would give PR 4 a
  permanently "unknown state" overlay while every tag looked healthy. value_type
  is derived per tag (bool before int -- isinstance(True, int) is True).
  source_system is plc_bridge; FactoryLM identity rides provenance.producer
  because VALID_SOURCE_SYSTEMS rejects factorylm-plc-modbus. The envelope's
  tenant_id is never read -- X-MIRA-Tenant is authoritative and the signed body
  omits it.

- tools/seeds/approved_tags_factorylm_conv_simple.sql: the 7 canonical
  conv_simple.* tags for source_system='plc_bridge'. A PREREQUISITE, not a
  follow-up: the allowlist is fail-closed with no permissive mode, so without
  the seed a valid snapshot is accepted with accepted=0, every tag
  not_allowlisted, nothing stored -- HTTP 200, no error, and every downstream
  check passes vacuously against an empty overlay.
  test_unseeded_allowlist_rejects_every_tag pins that failure mode explicitly.
  UNS identity comes from the seeded uns_path, never the envelope's
  proposed_uns_path (provenance only).

- mira-relay/auth.py: added sign_hmac_headers() and factored the signed string
  into one _signed_string() that both verify_hmac (server) and the signer
  (client) call, so the two cannot drift. Four producers already inline their own
  copy; rather than add a fifth, the new publisher uses the shared helper and a
  test round-trips its headers through the real verify_hmac. Migrating the
  existing four call sites is out of scope here.

- Runbook: staging-first validation with the seed as step 1 and
  "accepted=0 rejected=N" called out as the headline failure mode.

Read-only throughout: a snapshot is observation data; no fieldbus client is
imported, no command/actuator field is honored, no plant write occurs.

Tests: mira-relay/tests/test_factorylm_snapshot.py (28). mira-relay suite
191 -> 219 passed, zero regressions; test_architecture 13 passed (Contract 5
green -- the new module is NOT added to _ONE_PIPELINE_ALLOWLIST); PR 1's 15
tests still green. Pre-existing on origin/main and untouched here:
test_document_compiler determinism (1 failure) and 84 ruff errors in files this
PR does not modify.

The seed has been applied to NO environment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0191hTx4dwKR45av8njFyEPz
Mikecranesync pushed a commit that referenced this pull request Aug 2, 2026
… ingress (PRD #3048, PR 3)

PR 1 (v3.240.0) landed the envelope -> TechnicianContext.live adapter; this lands
the transport that actually delivers a snapshot. No new endpoint exists or could:
the one-pipeline law forbids a transport from defining its own normalizer,
allowlist, persistence, batch shape, or enforcement path, and test_architecture
Contract 5 fails the build on a violation. So the work is decode ->
build_tag_entry -> build_ingest_batch -> ingest_batch, the shape
simlab/publishers.py::RelayIngestPublisher established.

- mira-relay/factorylm_snapshot.py: snapshot_to_ingest_batch() (pure decode) +
  FactoryLMSnapshotPublisher (HMAC-signed POST to POST /api/v1/tags/ingest).
  Snapshot-scoped fields with no column of their own ride per-tag
  metadata.factorylm_snapshot; machine_state and active_conditions are REQUIRED
  to build a LiveStateOverlay, so losing them silently would give PR 4 a
  permanently "unknown state" overlay while every tag looked healthy. value_type
  is derived per tag (bool before int -- isinstance(True, int) is True).
  source_system is plc_bridge; FactoryLM identity rides provenance.producer
  because VALID_SOURCE_SYSTEMS rejects factorylm-plc-modbus. The envelope's
  tenant_id is never read -- X-MIRA-Tenant is authoritative and the signed body
  omits it.

- tools/seeds/approved_tags_factorylm_conv_simple.sql: the 7 canonical
  conv_simple.* tags for source_system='plc_bridge'. A PREREQUISITE, not a
  follow-up: the allowlist is fail-closed with no permissive mode, so without
  the seed a valid snapshot is accepted with accepted=0, every tag
  not_allowlisted, nothing stored -- HTTP 200, no error, and every downstream
  check passes vacuously against an empty overlay.
  test_unseeded_allowlist_rejects_every_tag pins that failure mode explicitly.
  UNS identity comes from the seeded uns_path, never the envelope's
  proposed_uns_path (provenance only).

- mira-relay/auth.py: added sign_hmac_headers() and factored the signed string
  into one _signed_string() that both verify_hmac (server) and the signer
  (client) call, so the two cannot drift. Four producers already inline their own
  copy; rather than add a fifth, the new publisher uses the shared helper and a
  test round-trips its headers through the real verify_hmac. Migrating the
  existing four call sites is out of scope here.

- Runbook: staging-first validation with the seed as step 1 and
  "accepted=0 rejected=N" called out as the headline failure mode.

Read-only throughout: a snapshot is observation data; no fieldbus client is
imported, no command/actuator field is honored, no plant write occurs.

Tests: mira-relay/tests/test_factorylm_snapshot.py (28). mira-relay suite
191 -> 219 passed, zero regressions; test_architecture 13 passed (Contract 5
green -- the new module is NOT added to _ONE_PIPELINE_ALLOWLIST); PR 1's 15
tests still green. Pre-existing on origin/main and untouched here:
test_document_compiler determinism (1 failure) and 84 ruff errors in files this
PR does not modify.

The seed has been applied to NO environment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0191hTx4dwKR45av8njFyEPz
Mikecranesync added a commit that referenced this pull request Aug 2, 2026
… ingress (PRD #3048, PR 3) (#3059)

PR 1 (v3.240.0) landed the envelope -> TechnicianContext.live adapter; this lands
the transport that actually delivers a snapshot. No new endpoint exists or could:
the one-pipeline law forbids a transport from defining its own normalizer,
allowlist, persistence, batch shape, or enforcement path, and test_architecture
Contract 5 fails the build on a violation. So the work is decode ->
build_tag_entry -> build_ingest_batch -> ingest_batch, the shape
simlab/publishers.py::RelayIngestPublisher established.

- mira-relay/factorylm_snapshot.py: snapshot_to_ingest_batch() (pure decode) +
  FactoryLMSnapshotPublisher (HMAC-signed POST to POST /api/v1/tags/ingest).
  Snapshot-scoped fields with no column of their own ride per-tag
  metadata.factorylm_snapshot; machine_state and active_conditions are REQUIRED
  to build a LiveStateOverlay, so losing them silently would give PR 4 a
  permanently "unknown state" overlay while every tag looked healthy. value_type
  is derived per tag (bool before int -- isinstance(True, int) is True).
  source_system is plc_bridge; FactoryLM identity rides provenance.producer
  because VALID_SOURCE_SYSTEMS rejects factorylm-plc-modbus. The envelope's
  tenant_id is never read -- X-MIRA-Tenant is authoritative and the signed body
  omits it.

- tools/seeds/approved_tags_factorylm_conv_simple.sql: the 7 canonical
  conv_simple.* tags for source_system='plc_bridge'. A PREREQUISITE, not a
  follow-up: the allowlist is fail-closed with no permissive mode, so without
  the seed a valid snapshot is accepted with accepted=0, every tag
  not_allowlisted, nothing stored -- HTTP 200, no error, and every downstream
  check passes vacuously against an empty overlay.
  test_unseeded_allowlist_rejects_every_tag pins that failure mode explicitly.
  UNS identity comes from the seeded uns_path, never the envelope's
  proposed_uns_path (provenance only).

- mira-relay/auth.py: added sign_hmac_headers() and factored the signed string
  into one _signed_string() that both verify_hmac (server) and the signer
  (client) call, so the two cannot drift. Four producers already inline their own
  copy; rather than add a fifth, the new publisher uses the shared helper and a
  test round-trips its headers through the real verify_hmac. Migrating the
  existing four call sites is out of scope here.

- Runbook: staging-first validation with the seed as step 1 and
  "accepted=0 rejected=N" called out as the headline failure mode.

Read-only throughout: a snapshot is observation data; no fieldbus client is
imported, no command/actuator field is honored, no plant write occurs.

Tests: mira-relay/tests/test_factorylm_snapshot.py (28). mira-relay suite
191 -> 219 passed, zero regressions; test_architecture 13 passed (Contract 5
green -- the new module is NOT added to _ONE_PIPELINE_ALLOWLIST); PR 1's 15
tests still green. Pre-existing on origin/main and untouched here:
test_document_compiler determinism (1 failure) and 84 ruff errors in files this
PR does not modify.

The seed has been applied to NO environment.


Claude-Session: https://claude.ai/code/session_0191hTx4dwKR45av8njFyEPz

Co-authored-by: MIRA Beta Orchestrator <beta-orchestrator@factorylm.local>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Mikecranesync

Copy link
Copy Markdown
Owner Author

🏁 PR 2 slice MERGED (factorylm repo)

Still open: MIRA #3058 (fixture-sync half of #199, all 27 checks green — awaiting merge). PR 2 slice is now fully landed; PR 3 (ingress + approved_tags seeding) and PR 5 (bench integration proof) remain unclaimed.

Mikecranesync added a commit that referenced this pull request Aug 2, 2026
…greed (#3060)

Two gaps from reviewing the PRD #3048 PR 1-3 work.

An empty tags list built a live overlay with no evidence in it. The FactoryLM
producer's validate_envelope already rejects tags: [] ("must be a non-empty
list"); the MIRA consumer accepted it and returned a LiveStateOverlay that
still asserted machine_state -- a live block capable of telling a technician
the machine is "running" with nothing behind it. The producer being stricter
than the consumer is backwards: the consumer faces untrusted input. tags:empty
is now a violation, so the turn degrades to "no live evidence" rather than a
fabricated plant claim.

The cross-repo fixture had no drift guard. contracts/machine_snapshot/ is
vendored verbatim in TWO repos and the PRD calls it the compatibility boundary
both projects must test the exact same payload against. It was byte-identical
at review time by care, not by mechanism -- and a one-sided edit drifts them
silently on BOTH sides, since each repo keeps passing against its own copy.
CHECKSUMS.sha256 + test_machine_snapshot_fixture_integrity.py make that fail
loudly; the guard also asserts every fixture on disk is listed, so a new
unguarded fixture cannot slip in behind a green check.

Proven by execution: 8 pass -> mutate a fixture -> checksum test fails with the
regeneration instructions -> restore -> 8 pass. materialized_evidence/tests/
195 passed + 8 new; ruff clean. No behavior change for any populated snapshot.

Co-authored-by: MIRA Beta Orchestrator <beta-orchestrator@factorylm.local>
Mikecranesync pushed a commit that referenced this pull request Aug 2, 2026
Wire the read-only FactoryLM live overlay into the ONE technician context
path. The snapshot is READ BACK at turn time from the persisted state carrier
(live_signal_cache) for the confirmed asset — never threaded inline from the
ingress, since ingest_batch persists rather than handing the engine a
request-scoped object. Additive, OFF by default (MIRA_FACTORYLM_LIVE); no
behavior change until enabled.

- mira-bots/shared/factorylm_live.py: fetch_live_signal_cache(tenant, prefix)
  reads current cache rows scoped to the asset UNS subtree (uns_path <@ ltree,
  tenant-scoped), mirroring ctx_enrichment; overlay_from_cache_rows(rows) builds
  a LiveStateOverlay — pure + deterministic. Freshness/timestamps come from the
  stored row, never now() (observed_at is the absolute last_seen_at), so two
  reads of the same rows produce a byte-identical overlay + stable manifest
  hash. simulated -> SIMULATED (never real telemetry); stale -> STALE (not
  dropped).
- Supervisor._build_factorylm_live_overlay reads the overlay back; the serving
  seam folds it into turn_ctx via augment_with_live so ONE manifest carries it,
  and renders [LIVE MACHINE STATE (FactoryLM)] ONLY when the fold succeeded —
  the prompt can never show live evidence the manifest lacks. When present it
  SUPERSEDES the legacy [LIVE EQUIPMENT STATUS] block (dedup).
- technician_context.live_prompt_block(overlay) renders only the live overlay.

Flag-gated + fail-open: any intake/contract error returns an ordinary answer
without live evidence. Read-only. Gated under contract_enabled() too, since the
overlay only reaches prompt+manifest via the contract's turn_ctx.

Tests: mira-bots/tests/test_factorylm_live_serving.py (14). Regressions green:
context_contract 27, technician_context/live/retrieval 54, ws1_engine_wiring 13,
materialized_evidence 185, architecture 13, legacy live wiring 15.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mikecranesync pushed a commit that referenced this pull request Aug 2, 2026
Wire the read-only FactoryLM live overlay into the ONE technician context
path. The snapshot is READ BACK at turn time from the persisted state carrier
(live_signal_cache) for the confirmed asset — never threaded inline from the
ingress, since ingest_batch persists rather than handing the engine a
request-scoped object. Additive, OFF by default (MIRA_FACTORYLM_LIVE); no
behavior change until enabled.

- mira-bots/shared/factorylm_live.py: fetch_live_signal_cache(tenant, prefix)
  reads current cache rows scoped to the asset UNS subtree (uns_path <@ ltree,
  tenant-scoped), mirroring ctx_enrichment; overlay_from_cache_rows(rows) builds
  a LiveStateOverlay — pure + deterministic. Freshness/timestamps come from the
  stored row, never now() (observed_at is the absolute last_seen_at), so two
  reads of the same rows produce a byte-identical overlay + stable manifest
  hash. simulated -> SIMULATED (never real telemetry); stale -> STALE (not
  dropped).
- Supervisor._build_factorylm_live_overlay reads the overlay back; the serving
  seam folds it into turn_ctx via augment_with_live so ONE manifest carries it,
  and renders [LIVE MACHINE STATE (FactoryLM)] ONLY when the fold succeeded —
  the prompt can never show live evidence the manifest lacks. When present it
  SUPERSEDES the legacy [LIVE EQUIPMENT STATUS] block (dedup).
- technician_context.live_prompt_block(overlay) renders only the live overlay.

Flag-gated + fail-open: any intake/contract error returns an ordinary answer
without live evidence. Read-only. Gated under contract_enabled() too, since the
overlay only reaches prompt+manifest via the contract's turn_ctx.

Tests: mira-bots/tests/test_factorylm_live_serving.py (14). Regressions green:
context_contract 27, technician_context/live/retrieval 54, ws1_engine_wiring 13,
materialized_evidence 185, architecture 13, legacy live wiring 15.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Mikecranesync

Copy link
Copy Markdown
Owner Author

🔒 Slice claim — PR 5 (controlled integration proof) (PLC laptop LAPTOP-0KA3C70H, Claude Code Fable 5 session)

  • Slice: PRD docs(prd): MIRA × FactoryLM Phase 1 — read-only machine evidence handoff #3048 PR 5 — the verification runbook + executable proof harness for the 7-point proof (simulated snapshot → ingress accept → TechnicianContext.live → prompt/manifest agreement → cited answer → fail-safe controls → no-write assertion). Analysis-first: PR 3 (ingress transport + approved_tags seeding) and PR 4 (serving path) are not yet built, so the deliverable will prove everything provable in-process today and precisely document what remains blocked on PRs 3/4.
  • Not touching: relay publishing to deployed envs, prod/staging seeding, engine serving-path implementation (PR 4's scope), no merges without Mike.

@Mikecranesync

Copy link
Copy Markdown
Owner Author

PR 5 slice built — MIRA #3062 (PLC laptop)

The verification slice per the PRD ("a separate verification PR/runbook update, not an implementation bundle"):

Merge order: #3061 (PR 4) → #3062 (PR 5) → then the supervised live probe. Remaining unclaimed: staging seed application + the probe itself (needs Mike).

Mikecranesync added a commit that referenced this pull request Aug 2, 2026
… 4) (#3061)

* feat(spine): FactoryLM live machine-state serving path (PRD #3048, PR 4)

Wire the read-only FactoryLM live overlay into the ONE technician context
path. The snapshot is READ BACK at turn time from the persisted state carrier
(live_signal_cache) for the confirmed asset — never threaded inline from the
ingress, since ingest_batch persists rather than handing the engine a
request-scoped object. Additive, OFF by default (MIRA_FACTORYLM_LIVE); no
behavior change until enabled.

- mira-bots/shared/factorylm_live.py: fetch_live_signal_cache(tenant, prefix)
  reads current cache rows scoped to the asset UNS subtree (uns_path <@ ltree,
  tenant-scoped), mirroring ctx_enrichment; overlay_from_cache_rows(rows) builds
  a LiveStateOverlay — pure + deterministic. Freshness/timestamps come from the
  stored row, never now() (observed_at is the absolute last_seen_at), so two
  reads of the same rows produce a byte-identical overlay + stable manifest
  hash. simulated -> SIMULATED (never real telemetry); stale -> STALE (not
  dropped).
- Supervisor._build_factorylm_live_overlay reads the overlay back; the serving
  seam folds it into turn_ctx via augment_with_live so ONE manifest carries it,
  and renders [LIVE MACHINE STATE (FactoryLM)] ONLY when the fold succeeded —
  the prompt can never show live evidence the manifest lacks. When present it
  SUPERSEDES the legacy [LIVE EQUIPMENT STATUS] block (dedup).
- technician_context.live_prompt_block(overlay) renders only the live overlay.

Flag-gated + fail-open: any intake/contract error returns an ordinary answer
without live evidence. Read-only. Gated under contract_enabled() too, since the
overlay only reaches prompt+manifest via the contract's turn_ctx.

Tests: mira-bots/tests/test_factorylm_live_serving.py (14). Regressions green:
context_contract 27, technician_context/live/retrieval 54, ws1_engine_wiring 13,
materialized_evidence 185, architecture 13, legacy live wiring 15.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(spine): ruff format the PR-4 serving-path additions

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(spine): preserve FactoryLM snapshot evidence at serving

* fix(spine): narrow FactoryLM snapshot metadata

---------

Co-authored-by: Mike Harper <bravonode@FactoryLM-Bravo.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: MIRA Beta Orchestrator <beta-orchestrator@factorylm.local>
Mikecranesync added a commit that referenced this pull request Aug 2, 2026
…runbook

The verification slice: chains the real merged modules in-process
(fixture → PR 3 transport → fail-closed ingress over the real seed →
PR 1 adapter → augment_with_live → one manifest) across all seven PRD
proof points. Served-path cases importorskip shared.factorylm_live so
they activate when PR 4 (#3061) merges — verified 23/23 green against
a scratch-merge of #3061; 20 passed + 3 skipped on today's main.

Runbook documents the proof matrix, dependency state, the supervised
staging live-probe procedure, and the deltas the proof must not paper
over. Hermetic: no network, no DB, no clocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n
Mikecranesync added a commit that referenced this pull request Aug 2, 2026
test(spine): PRD #3048 PR 5 — controlled integration proof harness + runbook
@Mikecranesync

Copy link
Copy Markdown
Owner Author

🏁 PR 5 MERGED — #3062 (0cc22b4a3, v3.242.1). PRD #3048 implementation arc is now fully landed: PR 1 #3052 → fixtures #3058 → PR 2 (factorylm #197/#198) → PR 3 #3059 (+#3060) → PR 4 #3061 → PR 5 #3062.

In-process proof runs 24/24 with zero skips on main — the served-path cases picked up the merged (hardened) PR 4 reader: identity-strict metadata.factorylm_snapshot requirement, source event_timestamp (never receipt time), machine_state served from stored metadata, generic cache rows never relabeled as FactoryLM evidence.

Remaining: the supervised live probe (runbook docs/runbooks/factorylm-machine-evidence-integration-proof.md). Two environment facts to decide on first, both verified today:

  1. Staging runs no relay — probe design runs the relay locally on the PLC laptop against the staging Neon branch.
  2. Staging bot tenant is the slug staging but this whole path is UUID-keyed (approved_tags, live_signal_cache casts) — the probe window needs MIRA_TENANT_ID=<UUID> on the staging bot (e.g. the stg quickstart tenant 78917b56-…). Mike's call, since staging KB scoping keyed to the slug may be affected.

Nothing is claimed production-proven until the probe passes under supervision.

Mikecranesync added a commit that referenced this pull request Aug 2, 2026
…NS resolution + QR certification

Live-probe finding (PRD #3048 PR 5): the vendor/model resolver returns
uns_path=None for equipment names like CV-101, and nothing else populated
uns_context.uns_path — so the FactoryLM live overlay's gate failed on
EVERY real chat turn AND every QR deep-link turn. PR 4's engine tests
mocked resolve_uns_path, which hid this.

- factorylm_live.uns_prefix_for_asset(tenant, *candidates): tenant-scoped
  cmms_equipment lookup (exact equipment_number first, then unambiguous
  description match only) — the same physical-subtree identity source the
  relay allowlist seed and the QR deep-link use. Fail-open, read-only.
- engine._build_factorylm_live_overlay: falls back to that lookup when the
  turn carries no uns_path (tag first, display label second).
- telegram/start_command: a QR scan now seeds the certified uns_context
  (source=direct_connection) with cmms_equipment.uns_path, per
  .claude/rules/direct-connection-uns-certified.md — previously it seeded
  only asset_identified/asset_tag, so even QR turns failed the gate.
- tools/factorylm_live_soak.py: N-iteration deterministic soak of the real
  engine seam + context fold (Celery-registrable callable), target 100%.
- tools/seeds/staging-cv101-probe.sql: idempotent staging CV-101 row so the
  probe subtree resolves (prod already has the garage bridge seed).

30 tests pass (test_factorylm_live_serving 24, test_start_command 6).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n
Mikecranesync added a commit that referenced this pull request Aug 2, 2026
…Rs 1-4 merged, PR 5 on bench (#3070)

Co-authored-by: Mike Harper <bravonode@FactoryLM-Bravo.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mikecranesync pushed a commit that referenced this pull request Aug 3, 2026
…n as live

Found by crossing the seam between PR 3 (#3059) and PR 4 (#3061) of PRD #3048
with a degraded tag -- something neither side's suite did, because each was
self-consistent on its own.

The shared fixture marks conv_simple.height_sensor_mm as quality="stale".
Through the DEPLOYED path (ingest -> live_signal_cache -> read-back) the overlay
returned it as Freshness.LIVE with freshness_summary {live: 7} -- zero stale. A
technician was shown, as current, a reading the producer had already flagged.

Cause -- two correct decisions that nobody joined. tag_ingest.persist_batch
stamps freshness_status = 'simulated' if simulated else 'live', which is right:
that column means COLLECTOR liveness, and a tag whose value is stale is still
arriving on time. factorylm_live._freshness_for then read ONLY that column,
never latest_quality -- which correctly held "stale" on the very same row.
Neither module was wrong in isolation, which is exactly why both suites stayed
green.

Fix: _freshness_for now takes latest_quality and downgrades on
{stale, bad, uncertain}, downgrade-only -- matching what PR 1's
overlay_from_factorylm_snapshot already did for the direct path. The band is
still computed from stored values; no now() at read time, and the
collector-liveness meaning of freshness_status is unchanged. The fix belongs in
the reader rather than the writer because liveness and value quality are two
different facts and both belong in the band the technician sees.

Regression cover, both mutation-verified (reverting the fix turns each red):

- test_stale_quality_never_becomes_live_ON_THE_DEPLOYED_PATH crosses ingest ->
  cache -> read-back with the degraded tag, asserting {live: 6, stale: 1} with
  all 7 tags still present (downgraded, never dropped). Its sibling
  test_stale_quality_never_becomes_live runs on PR 1's DIRECT adapter, which is
  why the defect survived.
- test_both_overlay_paths_agree_on_freshness pins the direct and deployed
  overlays to the same answer, so one snapshot can no longer yield two verdicts
  depending on plumbing the technician cannot see.

Suites: proof 24 -> 26, test_factorylm_live_serving 18, spine/context 242,
relay 232. No regressions. Pre-existing and untouched: ruff format drift in
tests/integration/test_machine_evidence_proof.py (lines 95/178/192/202 on main,
none in the added tests).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0191hTx4dwKR45av8njFyEPz
Mikecranesync added a commit that referenced this pull request Aug 3, 2026
…n as live (#3067)

Found by crossing the seam between PR 3 (#3059) and PR 4 (#3061) of PRD #3048
with a degraded tag -- something neither side's suite did, because each was
self-consistent on its own.

The shared fixture marks conv_simple.height_sensor_mm as quality="stale".
Through the DEPLOYED path (ingest -> live_signal_cache -> read-back) the overlay
returned it as Freshness.LIVE with freshness_summary {live: 7} -- zero stale. A
technician was shown, as current, a reading the producer had already flagged.

Cause -- two correct decisions that nobody joined. tag_ingest.persist_batch
stamps freshness_status = 'simulated' if simulated else 'live', which is right:
that column means COLLECTOR liveness, and a tag whose value is stale is still
arriving on time. factorylm_live._freshness_for then read ONLY that column,
never latest_quality -- which correctly held "stale" on the very same row.
Neither module was wrong in isolation, which is exactly why both suites stayed
green.

Fix: _freshness_for now takes latest_quality and downgrades on
{stale, bad, uncertain}, downgrade-only -- matching what PR 1's
overlay_from_factorylm_snapshot already did for the direct path. The band is
still computed from stored values; no now() at read time, and the
collector-liveness meaning of freshness_status is unchanged. The fix belongs in
the reader rather than the writer because liveness and value quality are two
different facts and both belong in the band the technician sees.

Regression cover, both mutation-verified (reverting the fix turns each red):

- test_stale_quality_never_becomes_live_ON_THE_DEPLOYED_PATH crosses ingest ->
  cache -> read-back with the degraded tag, asserting {live: 6, stale: 1} with
  all 7 tags still present (downgraded, never dropped). Its sibling
  test_stale_quality_never_becomes_live runs on PR 1's DIRECT adapter, which is
  why the defect survived.
- test_both_overlay_paths_agree_on_freshness pins the direct and deployed
  overlays to the same answer, so one snapshot can no longer yield two verdicts
  depending on plumbing the technician cannot see.

Suites: proof 24 -> 26, test_factorylm_live_serving 18, spine/context 242,
relay 232. No regressions. Pre-existing and untouched: ruff format drift in
tests/integration/test_machine_evidence_proof.py (lines 95/178/192/202 on main,
none in the added tests).


Claude-Session: https://claude.ai/code/session_0191hTx4dwKR45av8njFyEPz

Co-authored-by: MIRA Beta Orchestrator <beta-orchestrator@factorylm.local>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mikecranesync added a commit that referenced this pull request Aug 3, 2026
…NS resolution + QR certification

Live-probe finding (PRD #3048 PR 5): the vendor/model resolver returns
uns_path=None for equipment names like CV-101, and nothing else populated
uns_context.uns_path — so the FactoryLM live overlay's gate failed on
EVERY real chat turn AND every QR deep-link turn. PR 4's engine tests
mocked resolve_uns_path, which hid this.

- factorylm_live.uns_prefix_for_asset(tenant, *candidates): tenant-scoped
  cmms_equipment lookup (exact equipment_number first, then unambiguous
  description match only) — the same physical-subtree identity source the
  relay allowlist seed and the QR deep-link use. Fail-open, read-only.
- engine._build_factorylm_live_overlay: falls back to that lookup when the
  turn carries no uns_path (tag first, display label second).
- telegram/start_command: a QR scan now seeds the certified uns_context
  (source=direct_connection) with cmms_equipment.uns_path, per
  .claude/rules/direct-connection-uns-certified.md — previously it seeded
  only asset_identified/asset_tag, so even QR turns failed the gate.
- tools/factorylm_live_soak.py: N-iteration deterministic soak of the real
  engine seam + context fold (Celery-registrable callable), target 100%.
- tools/seeds/staging-cv101-probe.sql: idempotent staging CV-101 row so the
  probe subtree resolves (prod already has the garage bridge seed).

30 tests pass (test_factorylm_live_serving 24, test_start_command 6).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n
Mikecranesync added a commit that referenced this pull request Aug 3, 2026
…NS resolution + QR certification (#3068)

* fix(spine): live overlay reachable from real turns — equipment-name UNS resolution + QR certification

Live-probe finding (PRD #3048 PR 5): the vendor/model resolver returns
uns_path=None for equipment names like CV-101, and nothing else populated
uns_context.uns_path — so the FactoryLM live overlay's gate failed on
EVERY real chat turn AND every QR deep-link turn. PR 4's engine tests
mocked resolve_uns_path, which hid this.

- factorylm_live.uns_prefix_for_asset(tenant, *candidates): tenant-scoped
  cmms_equipment lookup (exact equipment_number first, then unambiguous
  description match only) — the same physical-subtree identity source the
  relay allowlist seed and the QR deep-link use. Fail-open, read-only.
- engine._build_factorylm_live_overlay: falls back to that lookup when the
  turn carries no uns_path (tag first, display label second).
- telegram/start_command: a QR scan now seeds the certified uns_context
  (source=direct_connection) with cmms_equipment.uns_path, per
  .claude/rules/direct-connection-uns-certified.md — previously it seeded
  only asset_identified/asset_tag, so even QR turns failed the gate.
- tools/factorylm_live_soak.py: N-iteration deterministic soak of the real
  engine seam + context fold (Celery-registrable callable), target 100%.
- tools/seeds/staging-cv101-probe.sql: idempotent staging CV-101 row so the
  probe subtree resolves (prod already has the garage bridge seed).

30 tests pass (test_factorylm_live_serving 24, test_start_command 6).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n

* fix(seeds): staging-cv101-probe — unquoted psql default + quoted-tenant repair

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n

* fix(seeds): staging-cv101-probe keys on the global equipment_number constraint

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n

* fix(seeds): tenant_id is pre-quoted by apply-seeds — reference it bare

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n

* fix(soak): live_prompt_block takes the overlay, not the combined context

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n

* chore(release): v3.242.3 + changelog for the live-overlay resolution fix

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n

* style(engine): ruff-format after rebase onto main

Format-only. The rebase onto current main surfaced drift that the later
chain commit used to carry; no behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Mikecranesync added a commit that referenced this pull request Aug 3, 2026
…an no longer fabricate plant state

Probe finding 2 (PRD #3048 PR 5): 'current state of my garage conveyor'
-> general_question (1.00) -> bare 80-token LLM call -> fabricated fault
+ error log for a healthy machine, KB-gap footnote appended after.

- Stage-0 deterministic asset-state override -> diagnose_equipment
  (tag-query fast-path precedent); UNS-gate precondition ORs the signal
  (resolver confidence is vendor-only, 0.0 for 'garage conveyor').
- Branch 3b: pre-generation refusal for plant-state questions on the
  general path - no LLM, honest no-live-data admission + handoff.
- Branch-5 prompt states no-live-connection; _INDUSTRIAL_HINTS_RE gains
  plant nouns; general exits carry citation_evidence for telemetry.
- Router prompt: asset-state = diagnose_equipment, CRITICAL RULE 7.

75 tests pass across touched suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n
Mikecranesync added a commit that referenced this pull request Aug 3, 2026
…th can no longer fabricate plant state (#3069)

* fix(engine): asset-state questions reach the UNS gate; general path can no longer fabricate plant state

Probe finding 2 (PRD #3048 PR 5): 'current state of my garage conveyor'
-> general_question (1.00) -> bare 80-token LLM call -> fabricated fault
+ error log for a healthy machine, KB-gap footnote appended after.

- Stage-0 deterministic asset-state override -> diagnose_equipment
  (tag-query fast-path precedent); UNS-gate precondition ORs the signal
  (resolver confidence is vendor-only, 0.0 for 'garage conveyor').
- Branch 3b: pre-generation refusal for plant-state questions on the
  general path - no LLM, honest no-live-data admission + handoff.
- Branch-5 prompt states no-live-connection; _INDUSTRIAL_HINTS_RE gains
  plant nouns; general exits carry citation_evidence for telemetry.
- Router prompt: asset-state = diagnose_equipment, CRITICAL RULE 7.

75 tests pass across touched suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n

* feat(engine): probabilistic asset-state arbitration replaces the binary override

Per Mike: routing is now a scored decision, not a single regex gate.
asset_state_probability() combines weak signals as log-odds (state
phrasing +2.4, asset noun +1.6, tag token +1.2, educational -2.6,
router vote +/-1.2/0.6 x confidence, prior -2.0) through a sigmoid;
score >= MIRA_ASSET_STATE_THRESHOLD (0.5) forces the gated path.
Deterministic math — same inputs, same score, never sampled — so the
behavior stays unit-testable; per-signal contributions are logged for
evidence-based threshold tuning.

Probe corpus clears the threshold against a maximally-confident
disagreeing router; educational questions score <=0.03; a bare asset
mention (~0.27) never forces the gate. 76 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n

* style(engine): ruff-format the arbitration + overlay hunks

Fixes the Lint & Format failure blocking CI Gate on #3069 — format-only,
no behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dgej4vU7mMnYTmU6QXsH3n

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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