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.
384 lines (345 loc) • 22.8 kB
text/typescript
/**
* # frame/template-record — the TemplateRecord evidence ledger (SKELETON: types + storage, ADR-0041 §2, kestrel-wa0j.26)
*
* `PerceptProfile` reduced to nothing more than a **TemplateRecord set** (ADR-0041 §2): the evidence
* ledger where every founder — or agent-authored — `.kestrel` template enters as a HYPOTHESIS carrying
* a cell-keyed ledger entry, NEVER a blessed default. This module is the record family + the in-memory
* store + fail-closed (de)serialization. It is a **skeleton by design**: no grading logic, no promotion
* logic, no harness — those land later (the matched-set harness owns selection/grading; ADR-0029 owns
* owner-gated freezes). What ships here is the SHAPE the evidence must take, and the shape is chosen so
* the passivity trap cannot hide in it.
*
* ## The cell key is a 5-tuple PHANTOM INDEX (ADR-0041 §2)
* Evidence keys on **instrumentClass × band × archetype-family × phase × role** (a {@link Cell}). It is
* a *phantom index*: there is NO coercion between cells, ever. Pooling grades across `role` (the pod
* {@link SeatId} axis) or across `phase` is ill-typed, because a pane's sign is role-dependent and
* phase-dependent. This module provides no API that merges two cells' evidence — cross-cell coercion
* has no surface, by construction. `role` is the closed {@link SeatId} roster (ADR-0041 A1); an unknown
* role is REFUSED at deserialization (fail-closed — the ledger keys on the role, so it can never admit a
* value outside the roster).
*
* ## The dual hash (ADR-0041 §2)
* A template carries TWO hashes, bound by one rule: `templateHash` (sha256 of the canonical PRINTED
* bytes) is the SKU — **identity** follows it; `elabHash` (sha256 of the ELABORATED/saturated form —
* every defaulted slot explicitly printed) carries **evidence**. A catalog-default change can move a
* template's denotation while its `templateHash` holds constant, but it changes the `elabHash`, so old
* grades can never silently re-attach to a template whose meaning drifted underneath them. This module
* takes both hashes as inputs (the printing lives in the renderer/lang, not here) — the skeleton records
* them, it does not compute the printed forms.
*
* ## Grades are POLE-VECTORS, never sums (ADR-0041 §2)
* The passivity trap is a *sign structure* — a restraint pane cuts the loss on the restraint pole while
* forfeiting the gain on the action pole, and a scalar that netted the two would hide it. So a cell's
* grade is a {@link PoleVector}: two separately-named sample vectors (restraint pole, action pole) with
* **no `sum`/`net` field**, and this module offers **no function** that reduces a PoleVector to a
* scalar. Scalar netting is inexpressible by construction, not by convention.
*
* Determinism: pure mapping + validation, no wall clock, no RNG (RUNTIME §0). Hashes are sha256 of
* canonical bytes; `committedAt` is a caller-supplied logical stamp, never `Date.now()` inside here.
*/
import { canonicalizeJson } from "../canonical/json.ts";
import { isSeatId, type SeatId } from "./seat.ts";
// ─────────────────────────────────────────────────────────────────────────────
// The cell key — the 5-tuple phantom index (ADR-0041 §2)
// ─────────────────────────────────────────────────────────────────────────────
/** The closed `instrumentClass` sort (ADR-0041 §2) — pinned NOW while the ledger is empty; opened only
* by the sort-introduction ceremony (§3). Every founder record carries `equity-option` until a
* non-equity cohort exists. */
export const INSTRUMENT_CLASSES = ["equity-option", "future", "perp", "fx-pair", "spot-equity"] as const;
export type InstrumentClass = (typeof INSTRUMENT_CLASSES)[number];
/** The closed `phase` axis (ADR-0041 §2) — orthogonal to band; a phase template is a named View
* delivered by a Wake. */
export const LEDGER_PHASES = ["OPEN", "WAKE", "SHOCK", "CLOSE"] as const;
export type LedgerPhase = (typeof LEDGER_PHASES)[number];
/**
* The evidence cell key — **instrumentClass × band × archetype-family × phase × role** (ADR-0041 §2).
* `band` and `archetype-family` are open working-set strings (their taxonomy is a measurement question
* for the first cohorts, ADR-0041 open questions #2), so they are typed as `string`; the two closed
* sorts (`instrumentClass`, `phase`) and the closed `role` roster ({@link SeatId}) are the schema this
* module enforces. Two cells are equal iff all five axes match — a phantom index, never coerced.
*/
export interface Cell {
readonly instrumentClass: InstrumentClass;
readonly band: string;
readonly archetypeFamily: string;
readonly phase: LedgerPhase;
readonly role: SeatId;
}
/** The canonical string key of a {@link Cell} — a stable, order-fixed join over the five axes, used as
* the ledger's per-cell map key. Because the axis order is fixed here in ONE place, two constructions of
* the same cell always key identically, and two DIFFERENT cells never collide (the phantom index). Pure. */
export function cellKey(cell: Cell): string {
// A NUL join over the fixed axis order — the axis strings can never contain NUL, so the key is
// unambiguous (no separator injection), and the order is pinned here so it never drifts.
return [cell.instrumentClass, cell.band, cell.archetypeFamily, cell.phase, cell.role].join("