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.
322 lines • 23.1 kB
TypeScript
/**
* # frame/pane-catalog — the ONE pane registry: single source for render / View / prompt (kestrel-4gl.13)
*
* Every available (non-kernel) pane is catalogued HERE, exactly once, with its metadata + its pure
* builder. This registry is what three consumers all read, so they can never drift:
* 1. the **renderer** ({@link ./render.ts}) MATERIALIZES panes from it;
* 2. the **View language** ({@link ../lang/ast.ts ViewStatement}) SELECTS panes from it (by id);
* 3. a **prompt** will later ADVERTISE the menu from it (`title` + one-line `description`).
*
* The **cockpit kernel is NOT in this catalog** — it is built in code and LEADS every frame,
* non-configurably, OUTSIDE any View selection (its reserved ids `kernel`/`cockpit` are refused,
* {@link ../frame/types.ts assertPaneIdAllowed}). This catalog is the *configurable body* only.
*
* ## Single-source guarantee (the whole point)
* A pane that is NOT in {@link PANE_CATALOG} CANNOT render (there is no other builder path), and a
* View naming an id absent from the catalog FAILS CLOSED ({@link resolveView}) — never a silent
* drop. Adding a new pane is one entry here; it is then selectable, renderable, and advertisable
* with no change anywhere else.
*
* ## Attribution class (charter) + token cost
* Each entry declares an {@link PaneAttribution} (`OBS` observed | `CALC` derived | `MODEL` a model
* output — a MODEL pane carries a receipt in its rendered text, e.g. the chain's `fair` fairNote)
* and a documented `tokenCostEstimate` (a rough chars/4 figure for the prompt menu + the View
* budget planner; the REAL cost is measured at materialization under the active tokenizer).
*
* ## Purity (ADR-0009)
* Every builder is a PURE function of its {@link PaneBuildContext} — no wall clock, no RNG; the same
* context renders a byte-identical block. Invents no value (`—` for the unknowns, {@link ./format.ts}).
*/
import type { SeatId } from "./seat.ts";
import type { Tokenizer } from "../render/index.ts";
import type { ArmedPlanTerms, InstrumentSpec, Kernel, MarketPane, PriorSessionTape, PriorVantage, TapeRow } from "./types.ts";
/** Everything a body-pane builder may read — a pure projection of the OPEN/WAKE frame input. A
* builder is a total function of THIS; it never reaches outside it (no wall clock, no RNG). */
export interface PaneBuildContext {
readonly instruments: readonly InstrumentSpec[];
readonly market: MarketPane;
readonly kernel: Kernel;
readonly frameKind: "OPEN" | "WAKE";
/** The prior author vantage's SERVED values (kestrel-wa0j.48) — a pure projection of the WAKE
* frame input's {@link ../frame/types.ts PriorVantage}. Present only on a WAKE frame the driver
* threaded a prior vantage into; the `delta` pane fails closed to absent-with-reason without it.
* Absent on OPEN and on any frame with no prior vantage (additive: those frames are byte-identical). */
readonly priorVantage?: PriorVantage;
/** Minutes since the author's last vantage (the WAKE frame's `minutesSinceLast`) — the `(<N>m ago)`
* cue on the `delta` header. Absent on OPEN / when unknown ⇒ the header omits the age (never a
* fabricated gap). PURE: a value carried on the frame input, not a wall-clock read. */
readonly minutesSinceLast?: number | null;
/** The prior sessions' tapes (kestrel-wa0j.20) — a pure projection of the frame input's
* {@link ../frame/types.ts PriorSessionTape}[]. Present only on a frame the driver threaded a multiday
* tape into; `tape d-N` serves ordinal N's rows as its OWN single-session block, else it renders the
* Train 1B absence line (byte-identical). Absent on a single-day frame (additive, byte-identical). */
readonly priorSessions?: readonly PriorSessionTape[];
/** The ARMED plans' enforced terms (kestrel-wa0j.29) — a pure projection of the frame input's
* {@link ../frame/types.ts ArmedPlanTerms}. Present only on a frame the driver threaded armed-plan
* terms into (a wake with a plan in an enforced state); the `armed-plan` pane fails closed to exactly
* one absent-with-reason line without it. Absent ⇒ byte-identical (the pane is catalog-only). */
readonly armedPlan?: ArmedPlanTerms;
}
/**
* CALC re-bucketing (kestrel-wa0j.19 §1): aggregate consecutive runs of `factor` served rows into
* ONE wider bucket. Deterministic AGGREGATION of real inputs — every number on an output row is a
* function of the input rows' own numbers (open = the run's FIRST open, high = max high, low = min
* low, close = the run's LAST close, clock = the CLOSING row's clock — the bucket closed there,
* volume = the sum WHEN every grouped row carries one; a partially-volumed group omits it rather
* than present a partial sum as the bucket's volume). A TRAILING PARTIAL run aggregates the rows
* that exist — it is real data, and nothing on it is invented. Pure; input order preserved.
* Exported so the aggregation semantics are directly testable (and reusable by the session lane).
*/
export declare function rebucketTape(rows: readonly TapeRow[], factor: number): TapeRow[];
/**
* TRUE iff a `tape <window>` arg is SERVABLE by a context whose rows arrive bucketed at `bucketMin`
* minutes: the window has a fixed minute width AND is a positive INTEGER MULTIPLE of the served
* bucket (equal ⇒ factor 1). The ONE shared validity predicate (kestrel-wa0j.19 §1): the session
* lane checks a scheduled View's window arg against it at SCHEDULE time (so a bad window is refused
* before it ever reaches an unguarded delivery render), and the arged tape builder enforces the
* SAME predicate at materialization — the two can never drift. Pure unit arithmetic, no context.
*/
export declare function windowServableBy(bucketMin: number, window: {
readonly value: number;
readonly unit: string;
}): boolean;
/**
* The ATM straddle **extrinsic** (time value remaining) off the near-money book — `gross(book mid) −
* intrinsic` at the strike nearest spot that carries BOTH a C and a P leg. Pure CALC over the OBSERVED
* chain + spot, mirroring the `vol` pane's decomposition (kestrel-wa0j.7). Returns `null` when it is
* NOT honestly computable: spot unknown, no ATM strike with both legs, a dark/one-sided ATM leg, or
* duplicate (strike,right) rows (an ambiguous book) — never a fabricated value. Exported + shared so
* the `delta` pane's CURRENT read and the driver's PRIOR-vantage capture compute the SAME number
* (they can never drift). The "never a price anchor" rule is respected: this is a labelled read of the
* book's own mids, never used to anchor an order price.
*/
export declare function atmStraddleExtrinsic(market: MarketPane): number | null;
/** The sell-floor POLICY line — the never-naked floor's decision-nearest spelling (close.txt POLICY).
* ONE definition: if the kernel ever renders a standing floor line, it must reuse THIS byte string so
* the two occurrences are byte-identical (ADR-0041 §1 exactly-twice channel invariant). */
export declare const CLOSE_SELL_FLOOR_POLICY = "sell floor=intrinsic; BELOW rows cannot anchor a sell";
/** A pane's attribution class (the Field/Attribution charter, 3-class): `OBS` an observed fact,
* `CALC` a derived quantity, `MODEL` a model output that carries a receipt in its rendered text. */
export type PaneAttribution = "OBS" | "CALC" | "MODEL";
/** A pane argument as the renderer consumes it — the structural mirror of the language's
* {@link ../lang/ast.ts PaneArg}, decoupled from the AST exactly as {@link ViewSelection} is (a
* caller can supply args from config without importing the language; the AST's own `PaneArg` is
* assignable to this shape). It carries only a **syntactic supersort** — a bare ident, a window
* `5m`, or a bare count numeral `12` — which a {@link ParamSlot} REFINES to a semantic sort at
* materialization (ADR-0041 §1: the parser assigns the supersort with zero catalog knowledge). */
export type PaneSelectionArg = {
readonly kind: "arg-ident";
readonly name: string;
} | {
readonly kind: "arg-window";
readonly window: {
readonly value: number;
readonly unit: string;
};
} | {
readonly kind: "arg-count";
readonly count: number;
} | {
readonly kind: "arg-ordinal";
readonly ordinal: number;
} | {
readonly kind: "arg-expiry";
readonly expiry: number;
};
/** A closed index sort a {@link ParamSlot} may denote (ADR-0041 §1). LIVE subset after Train 1B:
* `LevelName` / `Window` / `Count` / `SessionOrdinal` / `ExpiryOrdinal` — Train 1B opened
* `SessionOrdinal` (`tape d-N`) and `ExpiryOrdinal` (`chain e-N`) by the sort-introduction ceremony
* (ADR-0041 §3: teaching attestation + canonical-print/round-trip + denotation + consuming signature
* + reject fixture). `Instrument` is listed because it is pinned into the {@link POSITION_CLASS_ORDER}
* now (its slot arrives with the CALC cross-instrument lexeme, a later train — pinning ahead of need
* is the whole point: argument order is canonical bytes, hash-breaking to retrofit). The DEFERRED
* roster member `Band` is opened only by its own ceremony as its train lands — the roster is closed at
* any instant. */
export type ParamSort = "Instrument" | "LevelName" | "Window" | "Count" | "SessionOrdinal" | "ExpiryOrdinal";
/** The five closed SURFACE classes the parser assigns with zero catalog knowledge (ADR-0041 §1 — the
* `wf` rung). A {@link ParamSlot}'s semantic {@link ParamSort} refines FROM exactly one of these. */
export type SyntacticSupersort = "ident" | "window" | "numeral" | "ordinal" | "expiry";
/**
* THE ONE PINNED CROSS-PANE POSITION-CLASS ORDERING (ADR-0041 §1 — "one hypothesis, one hash").
*
* The single order in which sorts emit in a pane's canonical argument sequence, fixed ONCE across
* EVERY pane. Because printed argument order is part of the canonical bytes (and the template SKU is
* the sha256 of those bytes), this ordering is **hash-breaking to retrofit** and so is pinned here,
* in ONE place, before Train 1 prints its first canonical form.
*
* DERIVATION (reconciled with `tests/golden/accept/views.kestrel` + the Train 1B honest-absence
* teaching forms, NOT chosen by taste — ADR-0041 §1 "derived by reconciling with what views.kestrel
* already teaches"):
* • `Instrument` FIRST — it is the concord head (ADR-0041 §1 Concord: "every pane's Instrument
* agrees with the head"); the marked cross-instrument exception spells its two `Instrument` args
* ahead of everything else, so the head noun leads the phrase.
* • `LevelName` next — the named-address selectors (`levels vwap hod`) name WHICH addresses inside
* the already-fixed instrument.
* • `Window` next — the timescale scopes the temporal resolution of the addressed slice.
* • `Count` next — a cardinality over the already-addressed, already-scoped slice (`chain 12`).
* • `SessionOrdinal` next, then `ExpiryOrdinal` LAST (Train 1B, APPENDED — extend-only).
*
* WHY SessionOrdinal / ExpiryOrdinal are APPENDED (not inserted before Window/Count), despite a
* session/expiry selector reading semantically like an "addressing" class that might sit earlier:
* the ordering constant is **extend-only, existing ranks unchanged** — the hash-stability rule
* (ADR-0041 §1: printed argument order is canonical bytes, hash-breaking to retrofit). Inserting a
* sort mid-list would renumber `Window`/`Count` and rewrite the canonical bytes of the already-pinned
* Train-1A forms — forbidden. The COORDINATION is not free-floating: the Train 1B teaching forms
* `tape 5m d-0` (Window BEFORE SessionOrdinal) and `chain 12 e-0` (Count BEFORE ExpiryOrdinal) are
* the corpus evidence that PINS Window < SessionOrdinal and Count < ExpiryOrdinal — the append order
* is exactly what those canonical forms already print. `SessionOrdinal < ExpiryOrdinal` (session
* addressing outer to expiry selection; no single pane carries both, so their mutual order is inert,
* pinned session-before-expiry to match the `d-` before `e-` reading). Existing ranks — Instrument 0,
* LevelName 1, Window 2, Count 3 — are UNCHANGED, so no Train-1A canonical form re-hashes.
* views.kestrel contains no counter-example: its one multi-sort line (`tape skyline 5m vwap
* detector-strip`) is the DEFERRED full-tape per-pane position layout (style<window<overlay<strip —
* distinct position classes, LATENT until the multi-arg tape train), not the cross-pane SORT total
* order pinned here.
*
* ENFORCED (not merely documented): {@link paramSortRank} + a signature-order invariant test assert
* every catalog signature declares its slots in non-decreasing rank, so no pane can ever spell its
* args out of the pinned order.
*/
export declare const POSITION_CLASS_ORDER: readonly ParamSort[];
/** The rank of a sort in the pinned {@link POSITION_CLASS_ORDER} (lower prints earlier). */
export declare function paramSortRank(sort: ParamSort): number;
/** How many args a slot binds: exactly `one`, or `many` (variadic — MUST be the terminal slot of a
* signature, since a variadic slot consumes the rest of the positional args). */
export type SlotArity = "one" | "many";
/** One position class of a pane's signature (ADR-0041 §1). `name` labels the slot in a repair
* message; `sort` is the semantic sort an arg must elaborate to; `arity` is one/many; `default` is
* the elaborated value when the slot is unfilled (UNTOUCHED in Train 1A — no pane declares one, so
* the no-arg path stays byte-identical). */
export interface ParamSlot {
readonly name: string;
readonly sort: ParamSort;
readonly arity: SlotArity;
readonly default?: PaneSelectionArg;
}
/** One catalogued pane — the single source of truth entry. `id` is what a {@link
* ../lang/ast.ts ViewStatement} names; `title`/`description` are the prompt menu; `attribution`
* + `tokenCostEstimate` are planning metadata; `builder` is the pure renderer. */
export interface PaneCatalogEntry {
/** Stable pane id — what a View selects by, and what the prompt advertises. Never a reserved
* kernel id ({@link ../frame/types.ts assertPaneIdAllowed}). */
readonly id: string;
/** Human title for the prompt menu. */
readonly title: string;
/** One-line description for the prompt menu (what this pane shows). */
readonly description: string;
/** Attribution class (OBS | CALC | MODEL — a MODEL pane carries a receipt in its text). */
readonly attribution: PaneAttribution;
/** A documented rough token-cost estimate (chars/4 order-of-magnitude) for the prompt menu +
* View budget planning. The REAL cost is measured at materialization under the active tokenizer. */
readonly tokenCostEstimate: number;
/** The pure builder: a total function of the {@link PaneBuildContext}. */
readonly builder: (ctx: PaneBuildContext) => string;
/** The ordered {@link ParamSlot} signature (ADR-0041 §1 inflection substrate) — the closed index
* sorts this pane's positional args address. Its PRESENCE (with an {@link argedBuilder}) is what
* lets a View pass args; {@link bindArgs} elaborates the args against it at the `mat` rung. Absent
* ⇒ the pane takes no args ({@link resolveView} refuses any). Slots MUST be declared in
* non-decreasing {@link paramSortRank} (the pinned cross-pane ordering — enforced by test). */
readonly signature?: readonly ParamSlot[];
/** The ARGED builder — present ONLY on a pane with a {@link signature} (kestrel-wa0j.3 /
* ADR-0041 §1). It receives args ALREADY bound + sort-checked by {@link bindArgs} (the `mat`
* rung), and applies the `renderable`-rung VALUE checks that need the Frame (a window the served
* bucket cannot express; a count the frozen chain is short of; a level ident absent from the
* LevelSet), THROWING a {@link KernelHonestyError} naming pane + arg. Pure, exactly like `builder`. */
readonly argedBuilder?: (ctx: PaneBuildContext, args: readonly PaneSelectionArg[]) => string;
}
/** The ordered list of catalogued panes (single source of truth). New panes slot in here. */
export declare const PANE_CATALOG: readonly PaneCatalogEntry[];
/** Look up a catalogued pane by id, or `undefined`. The ONLY builder path — a pane not here cannot render. */
export declare function paneById(id: string): PaneCatalogEntry | undefined;
/** The OPEN keyframe's hardcoded pane order. kestrel-wa0j.47: `macro` left the default set — in the
* v1 harness the pane can only ever render its absence, and that absence already renders once in the
* kernel's unavailable-capabilities line (a non-safety fact renders exactly once, ADR-0041 §1). The
* pane stays catalogued and addressable: a View that names `macro` still renders absent-with-reason. */
export declare const DEFAULT_OPEN_PANES: readonly string[];
/** The WAKE delta frame's hardcoded pane order (byte-identical to the pre-catalog renderer). */
export declare const DEFAULT_WAKE_PANES: readonly string[];
/** The default (no View supplied) ordered pane ids for a frame kind — the current hardcoded set. */
export declare function defaultPaneIds(frameKind: "OPEN" | "WAKE"): readonly string[];
/** One resolved pane of a {@link ResolvedView}: its validated catalog id + the (possibly empty)
* args the View passed it — validated as UNDERSTOOD at resolution (only a pane with an
* `argedBuilder` may carry args), with the arg VALUES validated against the context at
* materialization (the builders are pure functions of the context; resolution has none). */
export interface ResolvedPane {
readonly id: string;
readonly args: readonly PaneSelectionArg[];
}
/** A View resolved against the catalog: an ordered list of validated panes (id + args) + an
* optional token budget. This is what the renderer materializes; the ViewStatement is resolved
* into it once. `panes` is the canonical field; `paneIds` is its id projection (kept for id-only
* consumers), derived ONLY by {@link resolvedViewOf} — never assembled by hand, so the two can
* never drift. */
export interface ResolvedView {
readonly name: string;
readonly paneIds: readonly string[];
readonly panes: readonly ResolvedPane[];
readonly budget?: number;
}
/** The minimal View selection shape the renderer consults — a subset of {@link
* ../lang/ast.ts ViewStatement} (name + ordered pane names, each with the ViewStatement's optional
* pane args + optional token budget). Decoupled from the AST so a caller can supply a View from
* config without importing the language (the AST's `PaneRef.args` is assignable to `args`). */
export interface ViewSelection {
readonly name: string;
readonly panes: readonly {
readonly name: string;
readonly args?: readonly PaneSelectionArg[];
}[];
readonly budget?: number;
}
/**
* Resolve a View selection against the catalog into an ordered, VALIDATED {@link ResolvedView}, or
* fall back to a founder seat View / the frame kind's DEFAULT View when none is supplied. FAIL-CLOSED:
* a pane id the View names that is absent from the catalog THROWS a {@link KernelHonestyError} (never
* a silent drop — a View can only select panes that exist); a View may never name a reserved kernel id
* ({@link ../frame/types.ts assertPaneIdAllowed}); and args passed to a pane that has no
* `argedBuilder` are REFUSED here (an arg is never silently dropped — kestrel-wa0j.3). Arg VALUES
* are validated against the build context at materialization (resolution has no context). Pure.
*
* ## Role-keyed resolution (ADR-0041 §2 / A1, kestrel-wa0j.26)
* The optional `seat` binds the acting pod {@link SeatId} for this frame. The PRECEDENCE is strict:
*
* explicit authored View > seat founder View > phase default
*
* 1. an explicit `view` ALWAYS wins — a seat never overrides an authored/scheduled/forced lens
* (`seat` is ignored when `view` is present);
* 2. else, when a `seat` is supplied AND it has a founder seed for this frame kind
* ({@link ./founder-registry.ts founderViewFor}), that founder View resolves — the graded SEED
* for the role (never a blessed default: it is delivered ONLY because the caller opted in);
* 3. else the frame kind's phase default (byte-identical to the pre-role-axis behaviour).
*
* NO-CHURN: `seat` is optional and defaulted absent, so EVERY existing call site (no seat) resolves
* byte-identically to before this axis existed — the founder branch is unreachable without a seat, and
* a seat with no founder seed for the frame falls through to the very same phase-default path. The
* config opt-in that decides whether the driver passes a seat at all lives in the driver
* (`AgentConfig.seatViews`), never here — this function only applies the precedence it is handed.
*/
export declare function resolveView(view: ViewSelection | undefined, frameKind: "OPEN" | "WAKE", seat?: SeatId): ResolvedView;
/**
* Render ONE catalogued pane through the real bind → build path — the exact per-pane step {@link
* materializePanes} drives for every pane in a resolved View, exposed as the public per-pane entry
* (kestrel-z473.4). It IS that path (materialization calls this), so the bind/compose layer gets
* covered by every pane test for free and the `builder`/`argedBuilder` handles no longer need to be
* reached directly: no args ⇒ the pure `builder`; args ⇒ {@link bindArgs} (the `mat`-rung sort/arity
* refusal, catalog-only + Frame-free) THEN the `argedBuilder` (the `renderable`-rung VALUE checks that
* need the Frame). FAIL-CLOSED: an `id` absent from the catalog THROWS a {@link KernelHonestyError}
* (a pane not catalogued cannot render — never a silent empty block), and args passed to an arg-less
* pane are REFUSED here exactly as materialization refuses them. Pure — every builder is a total
* function of `ctx` (no wall clock, no RNG). Output bytes are byte-identical to the direct handle call.
*/
export declare function renderPane(id: string, args: readonly PaneSelectionArg[], ctx: PaneBuildContext): string;
/**
* Materialize a resolved View into its ordered list of rendered body-pane blocks (the kernel is
* NOT here — it leads, non-configurably, in the renderer). Each pane is built PURELY from `ctx` via
* {@link renderPane} (the single per-pane bind → build path) and measured under `tokenizer`. BUDGET
* GUARD (fail-closed): if the View declares a token budget and the SELECTED panes' measured total
* exceeds it, this THROWS a {@link KernelHonestyError} naming the overage — never a silent drop of a
* pane the View asked for (an over-budget View is a defect the author must fix, exactly as the
* unknown-pane and kernel-lead guards fail closed). Pure.
*/
export declare function materializePanes(resolved: ResolvedView, ctx: PaneBuildContext, tokenizer: Tokenizer): readonly string[];
//# sourceMappingURL=pane-catalog.d.ts.map