⚡ Swarm Architecture

PROVEN — `swarph_shared.triage`: the producer layer

# 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 got degree 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 default 0 (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 proven workers/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 the state_log audit 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 FAILED in 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 emit 0-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 silent 0/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=false must 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.py files have diverged ~1,850 lines. /proven/ lands ONLY in the deployed /home/ubuntu/mesh-gateway/server.py (:8788). A /proven/ PR whose diff path starts swarph-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": "", // gateway clock — when captured (KEEP BOTH) "peer": "droplet", // SERVER-DERIVED from token, never asserted "app": "hedge-fund", // the dimension tuple — REQUIRED, validated at the door "service": "beta-trader", "feature": "order-execution", "function": "place_order", "kind": "measurement | ping | error", "payload": { ... } // kind-specific; three-state INSIDE (value | CNE, never a silent 0/null) } `
  • measurement → {metric, value|CNE, unit, ci}
  • ping → {} — the row's EXISTENCE at ts is 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):
1. CORRECT-value, described-wrong (a scoping error in PROSE, not data): DIRECTION was never broken — 57,252 (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 how position_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.py for 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's LEADS edges carry no built_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 via ts_recv (→ missing-bucket catches staleness); #290 must ADD built_at to every edge (LEADS lacks it; PMFG_EDGE has built_ts — the inconsistency hid it 5 days). And the graph producer has no cadence — analyze_price_contagion is 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 carries value | 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: CNE with a reason, never a silent 0/null.
  • Keep BOTH ts_emit and ts_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.triage own both arms, or is Property a sibling swarph_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).