# PROVEN — swarph_shared.triage: the producer layer
science-claude, 2026-08-04. The client + transport that feed PROVEN. Home decided by commander: swarph_shared.triage (the shared kernel every app already imports). Schema lives in result_contract_v1.md; this doc is the ARCHITECTURE.
⚠ Read first — the axis-honesty warning (droplet #15348, from building OMEGA's Z axis today)
OMEGA's 3D schema shipped with the time axis populated for 1 of 6 agents and the graph axis for 2 of 6 — and the ones that got filled are exactly the ones where the data was already lying around (Gamma got velocity because sentiment IS natively a time series; Contagion gotdegree because it IS natively a graph). The three that needed new work got zero — and the schema reported all six identically. The blindness correlates with implementation cost, so it concentrates exactly where nobody has looked: an instrument whose blind spots track difficulty always looks more complete than it is. PROVEN has the same gravity — it will fill the cheap axes (counters, gauges) and leave the expensive ones (propagation, causality) null while the schema presents them as equals. Decide that deliberately or inherit OMEGA's 1-of-3. ⚠ CORRECTION 2026-08-05 (see the GRAPH bullet + three-way distinction below): the graph-axis "absence" turned out to be THREE different failures — a correct value described wrong (store vs surface), a WRONG value (a default that read as a real 0), and a genuinely MISSING value — not one bug. So "verify the axis is honest before it has consumers" means three separate checks, not one: a 0 may be measured / a default-in-disguise / and absent is different from either. droplet and I each collapsed it once; the collapse is itself the hazard. And make each axis honest BEFORE it has consumers — building a reader against a partly-null axis bakes "null = flat" into every downstream measurement permanently; fixed after, it costs finding every consumer (commander's ordering rule, 2026-08-04).
Why here
PROVEN's founding risk is the empty store: a registry with no producer reports "nothing owes a measurement" == "everything measured" (#275, three live specimens #249/#264/#284).swarph_shared.triage IS the producer — apps already import swarph_shared, so instrumentation is a drop-in "snippet," not a new dependency or a discipline anyone can forget. Wiring as plumbing.
Shape — swarph_shared.triage IS a DATA LAYER (not a fire-and-forget beacon)
The app writes STRUCTURED observations into the layer ONCE (like a GTM dataLayer.push); the layer holds queryable current state and routes async to MANY sinks. The app never calls PROVEN directly — it writes to the data layer, which decouples producers from consumers.
`
app → swarph_shared.triage = THE DATA LAYER (one structured write; holds state)
├─ QUERYABLE now → dashboard reads current state (ALREADY the omega-triage pattern)
├─ HOLDS the schema → RED-on-silence knows what is owed
└─ routes async to SINKS:
├─ gateway /proven/* (validate-at-the-door → PROVEN server: registry #276 · ingest #277 · re-derive #278 · certify #279 · readout #283)
├─ local dashboard
└─ (future: cost, alerting — add a sink, NO app change)
`
One write, many readers. Adding a consumer never touches the app. The gateway pass-through is ONE sink, not the app's direct target.
Why a data layer (substrate, not framework)
Making triage a data layer makes observability a STRUCTURAL property: the app writes to the layer and monitoring EMERGES — rather than an additive property where the app must REMEMBER to call PROVEN. That is the substrate-vs-framework line: instrumentation you cannot forget because it lives where the app already writes its state.The rigor a data layer must carry (else it is a NEW silent-failure surface)
- The layer reports its OWN delivery health. Buffer overflow, gateway unreachable, a sink erroring → RED, never a silent drop. A data layer that quietly loses observations is the exact failure PROVEN exists to catch, one layer lower. Who watches the data layer? It watches itself, loudly.
- State is three-state. A property never written reads
COULD_NOT_EVALUATE/ unknown, never a default0(velocity=null / cost=0 at the state level). - Fail-safe for the app, fail-loud for observability. If the layer or a sink is down, the APP keeps running (the layer never blocks the trade); the degradation is RED so nobody mistakes a dark layer for a healthy-quiet app.
The GTM + GA4 mental model (the on-ramp — and where it stops)
Anyone who knows analytics already knows this architecture:| Analytics (GTM / GA4) | PROVEN |
|---|---|
| GTM dataLayer — app pushes structured data | swarph_shared.triage — app writes observations once |
| GTM tags / triggers — who fires on what | the layer's sinks + routing |
| GA4 property — per-site namespace | a PROVEN Property (per-app: hedge-fund, gridiron) |
| GA4 events + params/dimensions | metrics / events + dims |
| GA4 Measurement Protocol (collect) | gateway /proven/* pass-through (the beacon) |
| GA4 reports | PROVEN readout (#283) |
PROVEN is shaped like GA4 and INVERTED AT THE DOOR (lab #15281). Same topology, opposite default. An analogy imports the original's semantics with its shape, and GA4's defining behaviour is that the Measurement Protocol accepts what it is given — a malformed or nonsense hit returns 2xx and lands. That is the accepted-and-ignored family as a product decision, at planetary scale. So keep this sentence adjacent to the mapping every time the mapping appears, because "like GA4" and "the door must refuse" are in direct tension:
> GA4 defaults to ACCEPT and makes you find the bad data later; PROVEN defaults to REFUSE and makes the absence loud NOW. GA4 believes what it is sent; PROVEN re-derives, certifies, three-states, and refuses. The inversion is not a feature of PROVEN — it is PROVEN. Keep the analogy only while it makes the inversion vivid, never while it smuggles GA4's permissiveness in.
What PROVEN adds that GA4 has none of: re-derivation (reproduce vs ground truth, not receipt), certification (cross-vendor panel + verdict), three-state (COULD_NOT_EVALUATE first-class, never a silent 0), RED-on-silence, and refusal (abstain by name).
And the on-ramp that lands hardest: GA4's own worst failure mode is exactly what the registry guards against — a property with no hits configured reports ZERO, and zero is indistinguishable from "nothing happened." Every analytics veteran has been burned by that precise bug. It is #275 in a vocabulary the reader already trusts — a more persuasive statement of the empty-store trap than any argument from our own board.
Two arms, one package
"Triage" = the hedge fund's name; it generalizes to app-observability. Both arms emit into the same PROVEN registry.Arm 1 — errors (reactive): a LIFT, not a build
Generalize the hedge fund's provenworkers/triage_queue.py (+ triage_listener, triage_cli, dashboard hooks). What lifts as-is:
fingerprint(exception_type, top_frame, msg)dedup → issues collapse to one row.- ranking by severity × frequency × recency (
count,count_24h). - status lifecycle
new→triaged→approved→patched/needs_commander/wontfix, with thestate_logaudit table (from→to, actor, note) = provenance, already built. - the false-positive classifier — it already refuses to treat "a benign status summary reporting ZERO errors" or a gate
FAILEDin a table as an error. That is PROVEN's silent-success discipline living in production. Keep it first-class.
De-omega-ify (what's hedge-fund-specific, must parameterize):
- hardcoded journald services → pluggable ingest sources.
- hedge-fund log parsers → per-app parser registry.
- the autopatch allowlist/policy → injectable.
- queue path
/var/lib/omega-triage/queue.db→ per-app.
Arm 2 — measurements (proactive): the Property SDK
`python
from swarph_shared.triage import Property
hf = Property("hedge-fund", defaults=True) # defaults = the lie-detector bundle
hf.metric("open_positions", kind="gauge")
hf.metric("naked_shares", kind="gauge", red_if=">0") # the #167 failure, watched
with hf.measure("place_order", expect="fill<2s"): ... # auto-times; CNE if undeterminable
hf.emit("naked_shares", CNE, reason="TWAP tranche in flight") # never a silent 0
`
- defaults are lie-detectors, not vanity: heartbeat-by-independent-marker (not
process_alive), cost that cannot emit0-as-free (#244), decision→outcome arity (0-or-many, #205), errors-surfaced. - three-state at the source: un-computable metric →
COULD_NOT_EVALUATE+ reason, never a silent0/null. - RED-on-silence: an enrolled property that stops emitting is a fault, computed gateway-side against the derived population — not "no data."
The pass-through rule (keeps the collector honest)
/proven/emit validates at the door: three-state enforced, covers/does_not_cover required, a Result with no Enrollment is rejected, malformed emission fails LOUD. A gateway that accepts-and-stores is the receipt-not-re-derivation trap + the "200, does nothing" family (ai2/priority/cost=0). Identity is the peer token (server-derived measured_by, invariant 12 — free, same as board writes). Contract constraints (lab #15272, settled before code):
- Rejection is a 4xx that NAMES the missing enrollment — NEVER a 200-with-a-flag. Accept-and-mark is the accepted-and-ignored family (four instances on the board today: #256×3, #285). If the door validates, the door must REFUSE.
population_known=falsemust PROPAGATE through the gateway — never flattened to an empty list on the way. A relay that loses the cannot-evaluate bit turns PROVEN's abstention into a pass at the one hop nobody inspects. RED-on-silence is built first, not last: the pass-through refuses to report a green it cannot substantiate.- File discipline (HARD): the two
gateway/server.pyfiles have diverged ~1,850 lines./proven/lands ONLY in the deployed/home/ubuntu/mesh-gateway/server.py(:8788). A/proven/PR whose diff path startsswarph-cli/is refused on sight — it would be green everywhere and absent in production = #275 committed by the collector. lab states which file the diff touched as its first act on #288.
Data model — ONE envelope, captured as a TIME SERIES
All three kinds — measurement, alive-ping, triage-error — arrive through the data layer in ONE structured envelope (JSON/struct). A heartbeat is a measurement of "alive," an error a measurement of "broke," a metric a measurement of a value: one shape, one store.`json
{
"ts_emit": "2026-08-04T20:15:00.123Z", // peer clock — when it happened
"ts_recv": "`
- measurement →
{metric, value|CNE, unit, ci} - ping →
{}— the row's EXISTENCE attsis the signal (alive) - error →
{fingerprint, exception_type, top_frame, severity}(the triage arm)
Storage = TimescaleDB (already in the OMEGA stack: SQLITE + NEO4J + TIMESCALEDB — not a new dependency). Each received instance is one hypertable row; (app, service, feature, function, peer, kind) are indexed dimensions. Reporting + tracing are then time-series slices — a function's latency trail, a service's error rate this hour, a peer's ping cadence — by any dimension, over time.
Why time-series is the RIGHT store, not just convenient
Silence becomes STRUCTURALLY VISIBLE. RED-on-silence (inv. 2 / #275) stops being a special case and becomes a missing-bucket check: for each ENROLLED(app,service,feature,function) at its expected cadence, a gap in the series IS the fault; the alive-ping is the cadence anchor. Point-in-time telemetry cannot tell "healthy-quiet" from "dead"; a time series can — the empty-store guard computed over time, for free. This unifies registry and store: enrollment declares the expected series + cadence per dimension tuple; the store holds the actual; silence = expected − actual. The Result in result_contract_v1 is one kind of envelope; this generalizes it to all three.
Cadence is MANDATORY — the recursion, #275 one level in (lab #15325). Missing-bucket RED depends on the enrollment declaring a cadence. An enrollment with NO cadence makes "expected − actual" uncomputable, and the tempting default — compute nothing, report nothing — renders GREEN: the quietest row in the system while the least observed. Not "the registry is empty" but "the registry's rows are UNFALSIFIABLE." So: cadence is REQUIRED on any enrollment claiming a time series, else the enrollment itself is COULD_NOT_EVALUATE and says so by name. Silence must never be the output of a MISSING DENOMINATOR (a config gap → CNE); it must be the output of a KNOWN cadence with a MISSING bucket (a fault → RED). Two different facts; only one is a fault.
The 3D vector model — measures × time × graph (inherited from OMEGA's vector signal; commander + droplet)
The data layer is not a flat metric store — it is the 3D vector model the commander and droplet built for OMEGA's signals over months, applied to software health. A health measure is a VECTOR, not a scalar:- X / Y — base measures + dimensions (
app/service/feature/function× the metric). What is observed. - Z — TIME (TimescaleDB, #289). Per-measure velocity + tendency: is this measure trending / accelerating / decaying, broken down to each measure. Where "level can't tell flat from unmeasured" is resolved — the axis is the discriminator.
- GRAPH — flow + direction (Neo4j). Relationships across
app→service→feature→function(and peer→peer): does a degradation PROPAGATE, which way, how fast — failure contagion, not an isolated dot. PROVEN INHERITS this — OMEGA's graph STORE has direction, flow, and velocity. But it is THREE distinct defects, not one "insert bug" (droplet #16253, correcting my own over-collapse — the third framing of this axis, so precision matters):
(a)-[:LEADS]->(b) edges, granger_p 100%, trustworthy. droplet's #15348 "degree=0, no direction" was TRUE of the thin SIGNAL SURFACE (contagion degree, eclipse sources_hit = instant structural counts) and over-generalised to the STORE. A rich STORE and a thin consumer SURFACE are two objects; either can be the "graph axis" you mean — never say "graph" for one when you sampled the other.
2. WRONG-value (a DEFAULT, not a failed insert): lag_days=0 on all 57,252 — the insert SUCCEEDED, writing a default. granger emits best_lag; the store renamed it granger_lag; the writer read edge.get("best_lag", 0) → never matched → ,0 won every row. The zero HID it: lag_days=0 reads as "propagates instantly" — a confident wrong answer nobody questions; None would have been asked about. A default is a decision to answer when you do not know. Found once before on the snapshot path (2026-07-25); the Neo4j path wasn't — instance fixed, class left open. That gap IS PROVEN's reason.
3. MISSING-value (absence): te / te_net (flow magnitude + net direction) computed every run, never written — 9 measures computed, 3 stored. Not a lying-zero, an absence.
My one-liner "verify a 0 is measured-zero, not a broken write" catches ONLY (2) — it misses (3) (absent, not zero) and (1) (prose scoping, not data). The lesson: even a correction of a collapse can be a collapse; name WHICH failure mode.
Graph-axis guards PROVEN inherits (droplet #16253) — these are PROVEN capabilities, not notes
- a. A field 100%-populated AND 100%-single-valued is a DEFAULT, not a measurement — the shape of a constant. One query would have caught
lag_days; it is also howposition_track.regime='CALM'on 294,112 rows silently defeated every regime-conditional analysis in the fund. → a first-class PROVEN detector. - b. A computed measure that never reaches the store is invisible to (a). Guard at the WRITER: assert every per-subject computed measure is actually carried (droplet shipped
tests/test_leads_lag_key.pyfor exactly this — "remembering to publish is what gets forgotten"). → PROVEN's producer-side guard. - c. NO zero defaults on any measure. Absent must stay absent.
- d. Every stored artifact must carry its own
built_at/as_of; one that cannot date itself is UNMEASURED, not current (droplet #16308, board #316). This catches a FOURTH failure class — CORRECT-VALUE-STALE — that none of a/b/c catch: every field passes every value/presence check, but the value is OLD. OMEGA'sLEADSedges carry nobuilt_at, so a 5-day-old graph and a 5-minute-old one are byte-identical → a dead collector reads as a quiet estate (the exact RED-on-silence failure, on the graph). The time axis already has this viats_recv(→ missing-bucket catches staleness); #290 must ADDbuilt_atto every edge (LEADSlacks it;PMFG_EDGEhasbuilt_ts— the inconsistency hid it 5 days). And the graph producer has no cadence —analyze_price_contagionis LLM-elected (~1 call/week), so "freshness by the next rebuild" is a probability, not a schedule: the graph MUST self-date, because nothing schedules its refresh. The four failure classes together: described-wrong · wrong-value · missing-value · correct-value-STALE — PROVEN's job is telling all four apart from a real reading.
The #290 graph definition to inherit (droplet #16253)
DIRECTION = the edge (a)-[:LEADS]->(b) (+ granger_p significance) — live, 100%, trustworthy · STRENGTH = composite_strength (a scalar collapse — keep, NEVER the decision input) · VELOCITY = lag_days (Granger lag in days; today 0 = UNMEASURED, not instant — trustworthy only after the next price_contagion rebuild) · FLOW = te (transfer entropy volume) + te_net (net direction) — absent today, fixed, same rebuild caveat · also granger_f, correlation, xcorr_lag_days (distinct from the Granger lag), dy_weight (Diebold-Yilmaz) · separate edge type SPILLS_TO (horizon + weight, n=79).
OMEGA already runs exactly this stack — SQLITE + NEO4J + TIMESCALEDB — because it IS the vector model; PROVEN's data layer REUSES it (proven, not new). Payoff: a verdict upgrades from a scalar ("service X is red") to a trajectory ("X's errors are accelerating (Z) and cascading to Y (graph)") — the difference between knowing something broke and knowing what to do. The money-path lesson, complete: level (X/Y) + velocity (Z) + flow (graph); any one axis alone lies.
The Z axis — inherit droplet's shipped contract verbatim (#15348, built 2026-08-04)
droplet built OMEGA's velocity/Z axis today; these four are shipped-and-tested, inherit them: 1. Every reading carriesvalue | state (incl. explicit INSUFFICIENT) | n_obs | window_actually_covered, and COVERAGE travels with the values (n_entities / n_measured / coverage_pct in the same record). 0.0-from-6-points and unknown-from-1-point must never render the same. For observability this is sharper than for signals: a service that STOPPED emitting and one genuinely IDLE are the same null, and one is an incident — "ours cost a trade; yours costs a page that never fires." A consumer that cannot see "43% measured" reads a missing entity as a healthy one.
2. Store the number, derive the word. Numeric velocity_per_day (or per-unit) + a DERIVED state — never an enum-valued rate. You can always bucket a number; you cannot unbucket a word. And do NOT reuse a name that another producer already types differently (OMEGA's velocity is a str for two agents) — a shared name giving consumers different types per producer is a trap.
3. Extrapolation floor. You may not extrapolate a rate far beyond the window you measured it over — a 1-2 point move over 4.6 minutes reads as ±300/day (small-denominator artifact wearing a rate). Set the floor from the MEASURED window distribution, not taste. App metrics are burstier than signals — you hit this on the first scrape-gap. Below the floor, still report n_obs + window — a COVERAGE limit must not read as a missing signal, or rule 1 is rebuilt inside the fix for rule 3.
4. The average hides the shape. 5.0 → 8.0 → 5.1 is velocity +0.05/day, state STABLE but shape REVERSAL — moved up 3 and back 2.9, summary says nothing happened. Store the per-interval steps {dt, d_level, rate, to_level} (bounded to 24) and DERIVE a shape: STEP / DRIFT / REVERSAL / FLAT / INSUFFICIENT. A latency spike that recovers is exactly what you page on, and a mean rate erases it — one rate per measure per window is blind to every self-healing incident. (Live: STEP dominates DRIFT on every OMEGA agent — signals reprice in JUMPS; contagion 181 REVERSAL vs 14 FLAT. Infra will be even more step-shaped.)
droplet offered its velocity module — pure, 45 tests, no OMEGA deps beyond stdlib. PROVEN takes it as the basis for the Z-axis derivation (#289) rather than reimplementing.
Rigor the envelope carries
- Dimensions REQUIRED + validated at the door.
(app,service,feature,function)is the trace key; a row missing any of it cannot be traced/reported → 4xx that NAMES the gap, never a dimensionless accept. - Three-state in the payload —
value: CNEwith a reason, never a silent0/null. - Keep BOTH
ts_emitandts_recv— the GAP is itself a MEASUREMENT, not bookkeeping (lab #15325). It separates "producer fired late" from "transport was slow" from "clocks disagree" — three causes a single timestamp collapses into one unexplained number (a coincidence in the wrong timezone is indistinguishable from a cause). Never reconcile them into one at write time; if they disagree beyond a bound, SAY SO rather than trust either. - Immutable append. Capture as-received; the series IS the audit trail. Never mutate a row.
- Cardinality discipline. The dimension tuple is BOUNDED; high-cardinality identifiers (request IDs, tickers) live in
payload, not as dimensions — unbounded dimension cardinality kills a TSDB.
The seam (three-way AI²)
- Client
swarph_shared.triage(both arms) + the metric contract — science-claude; the error-arm lift is droplet's source (workers/triage_*is droplet lane). - Gateway
/proven/*pass-through + server-side registry/validation — lab. - Panel (#281) + attestation reporting-up (#282) — lab.
First app — the hedge fund, pointed not generic
Instrument the things that have already BITTEN: naked shares (#167), cost=0-as-free (#244), the control that couldn't report (#205), decision→outcome arity. A first property that earns its keep day one and dogfoods against real failure history. It's already dashboard-plugged, so the surface exists.Open
- Does
swarph_shared.triageown both arms, or isPropertya siblingswarph_shared.monitor? (Commander leans: one package, "triage".) - CAS backing for
reproducible_inputs(must survive to certification time — Q1 v1). - Migration: does omega-triage keep running while the generalized lift is proven side-by-side, then cut over? (avoid a big-bang; prove the lift certifies the same errors first).