kestrel.markets
Version:
A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.
42 lines (36 loc) • 3.06 kB
Markdown
# Attribution is a property of the series, carried by the registry — not a per-value Field
CONTEXT.md promises that every rendered value is a **Field** carrying its provenance, and
that a construct-time guard refuses a `MODEL` Field that omits its receipt. In the landed
code that guard (`makeField`, `validateClaims`) has no callers on the render path;
provenance is enforced only on the `MODEL` **claims** channel (`assertClaimHonest`, at
render), while `spot`, levels, `budget`, and the whole tape render as bare `number | null`.
So the "Frame invents nothing / provenance is pervasive" invariant is aspirational, not
enforced, and a pane's declared class is a hand-maintained annotation that can drift from
the value it labels. We reconcile this without wrapping every rendered value in a Field on
the hot render path — token-cheap perception is the whole point of the Frame.
Decision: a series' **Attribution** class (`OBS | CALC | MODEL`) is a property of its
**registry** definition, registered once alongside the series — `spot` is always `OBS`,
`vwap` always `CALC`, `fair` always `MODEL`. A rendered value's class is therefore
**derived by `series_id` lookup**, never declared per pane. The honesty guard keeps its
teeth exactly where a `MODEL` value is *produced* — the ExecutionFair engine, predictors,
regime writers — which is where `assertClaimHonest` already sits: a `MODEL` series' value
cannot be emitted without its receipt (source, model version, confidence). `OBS`/`CALC`
carry no receipt by definition, so they need no per-value wrapper. The null-is-UNKNOWN
discipline (absent-not-hidden, rendered `—`) is enforced once, in the frame builder.
Considered and rejected: **pervasive per-value Field** (the charter's literal reading) —
correct, but pays a Field-construction cost on every `OBS`/`CALC` number on the hot path,
for a class that is invariant per series and so belongs on the series, not the instant; and
**claims-channel-only** — cheapest, but leaves the pane's declared class hand-maintained
and able to lie, the exact gap this closes. Registry-seated attribution gets the honest win
— class derived, not declared — at the cost the "register once → every surface" series
doctrine already pays.
A per-instant reclassification (an `OBS` that degrades to `CALC` when its feed goes stale)
is **not** a counter-case: a stale value becomes the null Field (UNKNOWN); it does not
silently change class. If a genuine per-instant-varying attribution ever lands, it reopens
this ADR; until then, class is a series property.
Consequences: `makeField`/`validateClaims` (the dead constructors) collapse into one honesty
predicate shared with `assertClaimHonest`; the duplicate `Attribution`/`PaneAttribution`
enums become one; pane catalogs stop carrying an `attribution` string and derive it from
the series they render. CONTEXT.md's Field/Attribution entry is reconciled: provenance is
pervasive *by series*, enforced at `MODEL` production, not by a per-value guard on every
rendered number. (Arch review C4a + C4b.)