UNPKG

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
# 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.)