Data contracts between layers
Ten contracts carry data between layers, and repos may depend on nothing else: PlantState, Evidence, Prescription, Action, ValueRecord, AutonomyPolicy, Lot and genealogy, WriteRequest, ClosureState and Envelope. The L3 to L4 hop is Evidence and the L4 to L5 hop is Prescription; today they run on Finding 1.2.0 and the card proposal. Every message carries the Envelope header with its episode_id. A WriteRequest needs both a Prescription and a granted AutonomyPolicy. All ten are drafted as 0.x and move to 1.0.0 on first production use, and each section lists the fields and what exists in the code today.
Ten contracts carry the stack (D2). They are drafted in SE beside contracts 0.16.0 as 0.x, marked experimental; the first plant that uses one in production bumps it to 1.0.0 (section 5.11). Every message travels inside the Envelope header (section 5.10). Where an existing schema covers part of a contract, the section says so.
| Section | Contract | Produced by | Consumed by | First needed for |
|---|---|---|---|---|
| 5.1 | PlantState | Layer 3 estimators, Plant Box twin | Layer 4, writer, safety filter | Fast loop in shadow |
| 5.2 | Evidence | Layer 3 | Layer 4, agents, ledger | Slow loop |
| 5.3 | Prescription | Layer 4 (KR) | L5 cards, safety filter | Slow loop |
| 5.4 | Action | L5, writer | Ledger | Slow loop |
| 5.5 | ValueRecord | L5 verification | Customer, registry, autonomy | Slow loop |
| 5.6 | AutonomyPolicy | A person at the plant, through L5 | Safety filter, writer | Fast loop in shadow |
| 5.7 | Lot and genealogy | L2 linkers | Layer 3, ledger | Plants that track lots |
| 5.8 | WriteRequest | Layer 4 or a person | Safety filter, writer | Fast loop in shadow |
| 5.9 | ClosureState | L5 card machine | L6, analytics | Slow loop |
| 5.10 | Envelope | Every producer | Every consumer | Day one |
How to read it.
- All ten travel inside the Envelope (section 5.10), which is not drawn.
- A WriteRequest needs both a Prescription and a granted AutonomyPolicy.
- ValueRecords feed back into the policy: autonomy is earned from same-condition wins.
Build now: Evidence, Prescription, ClosureState. Later: PlantState, WriteRequest, AutonomyPolicy for the fast loop.
View Mermaid source
flowchart TB
%% house-style: contract-flow
subgraph inputs["State and evidence"]
direction LR
LOT["Lot and genealogy"]
PS["PlantState"]
EVI["Evidence"]
end
LOT --> EVI
PS --> EVI
EVI --> RX["Prescription"]
RX --> WR["WriteRequest"]
POL{{"AutonomyPolicy"}} --> WR
RX --> CS["ClosureState (cards)"]
WR --> ACT["Action"]
CS --> ACT
ACT --> VR["ValueRecord"]
EVI --> VR
VR -. "same-condition wins" .-> POL
classDef govc fill:#fff4d6,stroke:#c99a2e,color:#000
classDef agentc fill:#e8f0ff,stroke:#5b7bd5,color:#000
classDef loopc fill:#eef7ee,stroke:#4f9a4f,color:#000
class POL govc
5.1 PlantState#
The estimated state of an asset at a time, with uncertainty. Produced by layer 3 (twin runtime or estimators), consumed by layer 4 and the writer.
| Field | Type | Note |
|---|---|---|
plant_state_id, asset_id, ts | ids, timestamp | |
operating_state | enum: running, stopped, changeover, restart, unknown | From the fast-loop docs; writes allowed only in running |
state_vector | map name → {mean, std, unit} | Examples: thermal batch T_furnace, T_load; exchanger R_f, dR_f; filler mu_head_1..n |
covariance_ref | optional blob ref | Full covariance when needed |
model_ref | {model_id, version, params_version} | Ties to registry |
data_quality | {inputs_ok, gated_readings, last_good_ts} | From C03 |
coverage_label | enum: calibrated, under-covering, unknown | From replay coverage (for example, a nominal 90% interval that covers well under 90% of outcomes on replay is under-covering). Uncalibrated states cannot clear write gates (C44). |
evidence_label | per state_vector element: Measured, Estimated, Assumed, Unknown | Per element (recommended inside D13; it costs one field). Assumed or Unknown elements block action proposals through the router's abstain (C45) |
episode_id | id | Trace key across layers |
Exists today: nothing. twin_state table is DOCS.
5.2 Evidence#
A typed claim with its support. Replaces the energy-only FindingAs-built L3 detector output admitted to L4 (finding.json 1.2.0) as the unit of analytical output.
| Field | Note |
|---|---|
evidence_id, kind (detection, estimate, attribution, test_result, document) | |
subject (asset, lot, part, order) | Lot and part keys are new |
claim (structured: metric, direction, size, window) | Metric cites a semantic-metric version (C08) |
support (method, sample size, effect estimate with interval, p or posterior, assumptions list) | For causal claims, list assumptions explicitly |
tier (measured, confirmed, modeled, unknown) | Derived from the evidence labels by the mapping below (D13). The master document does not define tiers; SE owns this enum |
value_vector ({energy_kwh, energy_inr, scrap_kg, scrap_inr, throughput_units, quality_risk}) with ranges | Replaces the required kWh and INR pair |
model_ref, data_window, episode_id |
Exists today: finding.json (1.2.0, energy-centric) and finding-2.0.0.json (direction only).
Tier mapping (accepted under D13, 7 Oct 2026). The package uses two vocabularies. The contract tiers (measured, confirmed, modeled, unknown) are the provenance of a claim. The evidence labels (Measured, Estimated, Assumed, Unknown, plus calibrated or uncalibrated) are the status of each quantity inside it. A claim takes the weakest label among its load-bearing quantities, and only a passed check raises it to confirmed.
| Load-bearing quantities in the claim | Highest contract tier | Note |
|---|---|---|
| All Measured | measured | Direct meter, check-weigher or lab result |
| Measured or calibrated Estimated, plus a passed check (counterfactual method, or a named plant person) | confirmed | Estimated quantities must have passed C44 |
| Any Estimated without a check, or any Assumed | modeled | Assumed quantities are named in support.assumptions; uncalibrated Estimated cannot clear autonomy gates |
| Any Unknown | unknown | Shown only with the gap named; no action gated on it |
confirmed is not "verified". Only a counterfactual method that passed its checks (D5) may be called verified (section 1.1). A confirmation by a named plant person is recorded and shown as that person's confirmation, never as savings.
5.3 Prescription#
What to do, why, by whom, and how it will be checked.
| Field | Note |
|---|---|
prescription_id, episode_id, evidence_refs | Every number traces to an Evidence record |
what, why, who, when, impact, mv_plan | Exist in prescription.json today |
action_class (message, experiment, setpoint_change, schedule_change, maintenance) | New. schedule_change and maintenance are advice only: a named person accepts a near-term sequence before any write-back, and Stamped never authorises maintenance |
options (hold, step down, step up), each with predicted outcome and interval | New; hold is always an option |
required_autonomy_level (AL0–AL5) | New; compared with the AutonomyPolicy in force |
constraints_checked (list of envelope, state and FMEA checks with result) | New; filled by the safety filter |
expires_at | New; an expired prescription cannot become a WriteRequest |
Exists today: prescription.json with the first two rows.
5.4 Action#
What actually happened. One record per executed action, whether a person did it or the writer did.
| Field | Note |
|---|---|
action_id, prescription_id, write_request_id (writes only), episode_id | |
actor (person id, or writer id with policy grant id) | |
kind (message_ack, manual_change, confirmed_write, standing_write, experiment_step) | |
For writes: tag, old_value, requested_value, applied_value, readback_value, readback_ts, limits_applied {min, max, step, rate}, stop_condition, restored_value_on_stop | Fields missing from action-intent.json |
model_ref, plant_state_ref | What the system believed when acting |
outcome_window | When to judge it |
hash_prev, hash_self | Hash chain for write_log |
Exists today: closure/action-intent.json (leftover, missing the write fields; retired in favour of WriteRequestPlant Box write path request (direction; closure/action-intent.json retired), section 5.8). L5 card transitions record people's actions on cards.
5.5 ValueRecord#
The ledger row. One per action or per bundle, per KPI.
| Field | Note |
|---|---|
value_record_id, action_ids, kpi (energy, scrap, throughput, quality, gas) | Multi-KPI |
baseline_method (IPMVP option, ITS, DiD, synthetic control, switchback) and baseline_model_ref | |
normalisers (output, mix, shift, ambient) | As in L5 packs |
estimate with interval, units, inr_at_price_version | Price table versioned |
tier (measured, confirmed, modeled, unknown) | "Verified" only for counterfactual M&V that passed its model-fit checks |
status (pending, ops_confirmed, signed_off, disputed, superseded) | Lifecycle only; how strong the claim is lives in tier. Migration: L2 verified becomes signed_off where a sign-off exists, otherwise ops_confirmed; contract modeled becomes tier = modeled with status = pending (D2) |
customer_signoff | Who signed, when |
Exists today: ledger.mv_ledger (L2, energy-only, keyed by prescription), closure/ledger-entry.json (energy-only, different enum), L5 verification packs.
5.6 AutonomyPolicy#
The grant of authority.
| Field | Note |
|---|---|
policy_id, asset_id, action_class, level (AL0 to AL5) | |
operating_envelope (tags, min, max, step, rate, allowed operating states) | Read by the safety filter (D15) and the writer |
preconditions (min same-condition wins recorded in the ledger, coverage ≥ target, model version pinned, data quality ok) | The L5 gate's min_verified_same_condition field implements the first item |
stop_conditions (operator touch, heartbeat lost, state change, coverage drop) | |
granted_by (person, role, at the plant: yes or no), granted_at, expires_at, revoked_at | Switching on needs a person at the plant |
fmea_refs | One hazard row per tag (fast-loop docs) |
Exists today: the card-transition gate in L5. No contract.
5.7 Lot and genealogy#
Which units, parts and lots passed through which assets, and how sure the link is. Stored as relational and event tables in L2 (D1).
| Field | Note |
|---|---|
lot_id, part_id (optional), order_id (optional), material_ref | Keys used by Evidence subject |
event_id, asset_id, start_ts, end_ts, event_kind (cycle, load, test, stop, changeover) | Event graph (Option C) |
link {from, to, link_method (scan, time_window, head_index, manual), link_confidence 0–1, link_status (suggested, approved, rejected)} | Cognite annotation pattern (E29); low-confidence links never gate an action |
source_ref, episode_id | Where the link came from |
Exists today: L2 context records (migration 014) and topology, stops and changeovers (016). No lot or link tables.
5.8 WriteRequest#
A request to change one set point, following the NE 178 Verification of Request pattern at pattern level (D17). The writer accepts nothing else.
| Field | Note |
|---|---|
write_request_id, prescription_id, policy_id, episode_id, idempotency_key | Duplicate keys are ignored |
tag, requested_value, unit, requested_by (person or standing policy) | OPC UA node only (D10) |
status (requested, authenticated, authorised, verified, mapped, accepted, applied, read_back, rejected, expired, stopped) | VoR flow; each step writes a timestamp and reason |
rejection_reason (envelope, operating_state, coverage, stale_model, operator_touch, expired, clock_skew) | Status feedback without plant internals |
expires_at | AL2 confirmation window; standing grants use the policy expiry |
Exists today: nothing. closure/action-intent.json is retired.
5.9 ClosureState#
The card lifecycle the L5 machine already runs, published so L6 and analytics stop guessing. Values are the code values in stamped_l5_domain/cards/states.py.
| State | Who sees it | Terminal |
|---|---|---|
open, assigned, in_progress | Customer | No |
closed_verified | Customer | Yes (can take a regressed self-transition) |
closed_no_change, closed_unverified, rejected | Customer | Yes |
deferred_expired with sub-state deferred or expired | Customer | Only when expired |
blocked_disputed | Customer | No |
pending_stamped_review | Staff only | No |
withheld_by_staff | Staff only | Yes |
closed_verified is a code name: it means the L5 verification pack saw the change in telemetry, which is ops_confirmed in ledger terms, not counterfactual verification. The contract keeps the code value and documents this; a rename waits for 1.0.0. The 8-state description in older docs is retired.
5.10 Envelope#
The header every contract travels in.
| Field | Note |
|---|---|
contract, contract_version | For example PlantState, 0.3.0 |
tenant_id, plant_id | RLS keys (section 12) |
message_id, idempotency_key, source_seq | Dedupe and replay (section 7.3) |
produced_at, source_ts, received_at | Clock handling (section 7.3) |
producer {repo, component, version} | Traceability |
episode_id | Trace key across layers |
5.11 Versioning and compatibility#
- Semantic versions per contract. While
0.x, a minor bump may break; consumers pin the minor version. - From
1.0.0: additive fields are minor; removing or renaming a field, or changing an enum value, is major and needs a migration note and one release where both versions are accepted. - Unknown fields are ignored by consumers; missing required fields are rejected at the seam, never defaulted.
- SE CI validates every schema and its examples (the existing contract-check workflow).
Page history: last 5 changes
- docs(research): retire stale research to archive/research-2026-10 with a register
ab84821 - docs(technical): rewrite fast-loop/; all architecture diagrams in house style
7330f47 - docs(technical): archive archify; add SYSTEM_VIEWS.md house diagrams; check_docs --min
1e190b6 - docs(technical): carry product sections; rewrite README and pointers
ee1e818 - docs(technical): split decision board into DECISIONS.md
b4db9d4