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.
901 lines (800 loc) • 41.7 kB
text/typescript
/**
* # ast — the typed object model IS the language (ADR-0004)
*
* Kestrel's canonical representation is this discriminated-union family, not the text.
* `parse(text) -> objects` (phase 3) and `print(objects) -> text` (this phase, in
* `print.ts`) are inverse projections with a byte-stable round-trip. Every node carries a
* literal `kind` tag; there are no classes (data is plain readonly objects) and no `any`.
*
* One language, four statement kinds (ADR-0001): {@link ViewStatement},
* {@link WakeStatement}, {@link PlanStatement}, {@link GradeStatement} — plus the org
* nodes {@link PodStatement} / {@link BookStatement} (ADR-0002). All four surfaces share
* one lexical core: the {@link Trigger} algebra, {@link SeriesRef} operands, and
* {@link PriceExpr} price expressions.
*
* The AST is ALWAYS fully qualified — `USING` defaults are resolved at parse time, and
* the printer elides whatever matches the ambient `USING` (ADR-0004; ARCHITECTURE §2).
*/
// ─────────────────────────────────────────────────────────────────────────────
// Comment trivia — the byte-stable "why" that travels WITH the action (ADR-0033)
// ─────────────────────────────────────────────────────────────────────────────
//
// Comments are NOT executable (they carry no runtime semantics); they are authored reasoning
// that MUST survive `print(parse(text))` byte-for-byte (ADR-0004 round-trip; ADR-0013:63-65
// depends on it; ADR-0031 journals through it). They are modeled as a cross-cutting SIDECAR
// on the block nodes that print as one or more lines — never fields on the semantic clause
// nodes — so the object model of triggers/prices/legs stays comment-free and every existing
// builder and deep-equality test is untouched (the sidecar is simply absent on comment-free
// documents).
//
// Two kinds, by position:
// • OWN-LINE (leading/tail): a line whose first non-space char is `#`. Printed on its own
// line at the block/clause indentation, ABOVE the line it precedes (leading) or after the
// block's last line (tail).
// • TRAILING-INLINE: a `#…` after the statement/clause tokens on the same line. Printed
// after the line content with the canonical single space before `#`.
//
// A comment's `text` is captured VERBATIM: everything after the opening `#` to end of line,
// the `#` itself excluded (so `# why` stores " why" and reprints as `# why`, byte-identical).
// The line INDENT and the single space before a trailing `#` are the only things canonicalized.
/** One printed line's comment trivia. `at` is the line's ordinal WITHIN its owning block, in
* canonical print order: `0` = the block's own header line; `1..N` = the block's direct
* interior lines (USING/WHEN/BECAUSE/clauses/panes/risk/directives). Child blocks own their
* own trivia, so they never consume an ordinal here. */
export interface LineComment {
readonly at: number;
/** Own-line comments printed on their own line(s) ABOVE line `at`, verbatim, `#` excluded. */
readonly leading?: readonly string[];
/** An inline comment printed after line `at`'s content, one canonical space before `#`. */
readonly trailing?: string;
}
/** The comment sidecar for a block node (a statement, or the module). Absent entirely on a
* comment-free document, so it never perturbs deep-equality against builder output. */
export interface CommentLayer {
/** Comments bound to specific printed lines of this block, by ordinal. */
readonly lines?: readonly LineComment[];
/** Own-line comments trailing the block's last line, at the block's interior indentation. */
readonly tail?: readonly string[];
}
// ─────────────────────────────────────────────────────────────────────────────
// Primitive lexical units
// ─────────────────────────────────────────────────────────────────────────────
/** Timescale unit for windows and durations. There is no absolute shock: magnitude is
* judged at a window (CONTEXT: Window). */
export type TimeUnit = "s" | "m" | "h" | "d" | "w" | "mo" | "q" | "y";
/** The timescale parameter a rate/magnitude series carries — `velocity(1m)`. */
export interface Window {
readonly kind: "window";
readonly value: number;
readonly unit: TimeUnit;
}
/** A span of time used by `held`, `within`, `esc`, and relative `ttl`. */
export interface Duration {
readonly kind: "duration";
readonly value: number;
readonly unit: TimeUnit;
}
/** A wall-clock time of day (session calendar), `HH:MM`. */
export interface TimeOfDay {
readonly kind: "time-of-day";
readonly hour: number;
readonly minute: number;
}
/** A numeric quantity with an optional unit. Plain number, risk-fraction `R`, cents `c`,
* percent `%`, or basis points `bp`. Used as a trigger operand and in offsets. */
export interface Quantity {
readonly kind: "quantity";
readonly value: number;
readonly unit?: "R" | "c" | "%" | "bp";
}
/** A baseline-relative threshold — the portable, cross-instrument form of "big"
* (CONTEXT: Window). `p99` = 99th percentile of the window's own trailing baseline;
* `3sigma` = three standard deviations. */
export interface Baseline {
readonly kind: "baseline";
readonly stat: "p" | "sigma";
readonly n: number;
}
// ─────────────────────────────────────────────────────────────────────────────
// Series references — the universal operand (CONTEXT: Series, Registry)
// ─────────────────────────────────────────────────────────────────────────────
/** A selector on a path segment: an org quantifier (`children(any)`) or a named target
* (`plan(chase-urgent)`). */
export type PathSelector =
| { readonly kind: "sel-quant"; readonly q: "any" | "all" }
| { readonly kind: "sel-name"; readonly name: string };
/** One dotted segment of a series path, optionally selected. */
export interface PathSegment {
readonly name: string;
readonly selector?: PathSelector;
}
/**
* A named series whose value changes over time — the universal operand.
*
* Two kinds and only two (CONTEXT: Series): **market facts** (ambient per signal
* instrument, spectator-visible: `spot`, `vwap`, `velocity(1m)`) and **org facts**
* (path-scoped in the pod tree, acting-sessions only: `pnl`, `alpha.pnl`,
* `fills.avg_px`, `plan(chase-urgent).state`). The market/org distinction is **registry
* resolution metadata, not a syntactic property** — canonical text does not mark it (a
* dotted path prints identically for both), so it is NOT a field on this node; the
* registry (a later phase) resolves a path to its space. A bare symbolic value like a
* plan-state literal (`armed`) is a single-segment series — indistinguishable in text
* from any other name, and resolved by the registry (ADR-0004: the node is the syntactic
* identity; round-trip is mechanical, so nothing un-printable lives here).
*/
export interface SeriesRef {
readonly kind: "series";
readonly segments: readonly PathSegment[];
readonly window?: Window;
}
/** Any leaf value that a comparison or cross can stand on. A bare identifier operand (a
* plan-state literal like `armed`) is a single-segment {@link SeriesRef} — text cannot
* distinguish it from a series, and ADR-0004 forbids un-round-trippable distinctions. */
export type Operand = SeriesRef | Quantity | Baseline;
// ─────────────────────────────────────────────────────────────────────────────
// Trigger algebra — the shared WHEN language (CONTEXT: Trigger algebra)
// ─────────────────────────────────────────────────────────────────────────────
export type CompareOp = "gt" | "ge" | "lt" | "le" | "eq" | "ne";
/** `left OP right` — a level/series comparison. */
export interface Comparison {
readonly kind: "cmp";
readonly op: CompareOp;
readonly left: Operand;
readonly right: Operand;
}
/**
* `left crosses above|below right` — an edge event, not a level.
*
* Two hardening dials, both learned from a graded-sim failure where a bare cross phantom-
* fired 10/12 on a single anomalous print:
*
* - **`touch`** picks the boundary predicate. Omitted (the default) is a **strict** cross:
* the series must move strictly past `right` (`>`/`<`). Present (`touches above|below`) is
* an **at-or-touch** cross: reaching *or* touching the level fires (`>=`/`<=`).
* - **`band`** is a **re-arm** width. With a band, after the cross fires the series must
* clear back past `right ∓ band` (below `right−band` for an `above` cross, above
* `right+band` for `below`) and **re-arm** before it can fire again — so a lone spurious
* tick cannot phantom-fire. Omitted = a bare, single-shot cross (the fragile form). A
* positive magnitude; a zero/negative band cannot re-arm and is refused at parse time.
*
* Doctrine (fail-closed, all surfaces): a cross is an edge, not a level, so it may **not**
* be wrapped in `held` — sustaining an edge for a duration is the anti-pattern that phantom-
* fired; the parser rejects it, the builder refuses to construct it, the printer throws on
* it. Robustness comes from `band` (re-arm), not from holding the edge. (`within` over a
* cross stays legal: "the cross occurred inside this window" is a well-formed question.)
*/
export interface CrossEvent {
readonly kind: "cross";
readonly left: Operand;
readonly dir: "above" | "below";
readonly right: Operand;
readonly touch?: boolean;
readonly band?: Quantity;
}
/** Break-and-hold: `inner held <dur>` — the inner condition sustained for a duration. */
export interface BreakHold {
readonly kind: "break-hold";
readonly inner: Trigger;
readonly dur: Duration;
}
/** `inner within <dur>` — the inner event occurred inside a rolling window. */
export interface WithinWindow {
readonly kind: "within";
readonly inner: Trigger;
readonly dur: Duration;
}
/** `inner until <clock>` — the inner expectation is in force up to a wall-clock time; the
* thesis temporal envelope (kestrel-rtf). A postfix combinator over the SAME predicate
* surface as {@link WithinWindow} — `until`/`at` are siblings of `within`, not a second
* predicate language — each carrying a {@link TimeOfDay}. */
export interface UntilWindow {
readonly kind: "until";
readonly inner: Trigger;
readonly at: TimeOfDay;
}
/** `inner at <clock>` — the inner expectation is evaluated at a wall-clock time; the thesis
* temporal envelope (kestrel-rtf). A postfix combinator sibling of {@link WithinWindow}. */
export interface AtWindow {
readonly kind: "at";
readonly inner: Trigger;
readonly at: TimeOfDay;
}
/**
* `held <dur>` in EXIT/lead position — a 0DTE **time-held stop**: the position has been HELD
* for a duration (measured from the plan's first acquiring fill), independent of any market
* predicate. This is the in-grammar form for "exit N minutes after entry" the FOMC-options
* single-model runs reached for out-of-grammar (`EXIT held 90m`, `EXIT held 120m`) and
* parse-escaped on, de-arming the whole plan (docs/results/fomc-options-axis/report.md).
*
* It is NOT a {@link BreakHold}: break-hold sustains an INNER predicate for a duration (any
* flicker resets the clock); a `held-stop` has no inner predicate — it is a bare hold-clock on
* the acquired inventory. As a general {@link Trigger} it only has a referent inside a clause
* that manages a held position (EXIT); read in any other position it fails closed (UNKNOWN),
* exactly like {@link UntilWindow}/{@link AtWindow}. The engine fires it at the leg's own basis,
* respecting never-naked / covered-close, through the SAME {@link ExitClause} path.
*/
export interface TimeHeldStop {
readonly kind: "held-stop";
readonly dur: Duration;
}
/**
* `clockET <clock>` — a 0DTE **wall-clock time-stop**: fire at or after a wall-clock ET time
* (the theta-into-the-close cut the FOMC-options runs authored out-of-grammar as
* `EXIT clockET 15:40`). Semantically a sibling of `time after <clock>`, but named `clockET`
* because that is the surface the single-model authors emitted (mirroring the watcher's
* `scheduleWake at {kind:"atClockET"}` vocabulary). Carries a {@link TimeOfDay}.
*/
export interface ClockStop {
readonly kind: "clock-stop";
readonly at: TimeOfDay;
}
/** The Nth occurrence of an event: `second failed-break of HOD`. */
export interface NthEvent {
readonly kind: "nth";
readonly ordinal: number;
readonly event: Trigger;
}
/** A named structural market event, optionally of a level: `failed-break of HOD`. */
export interface StructuralEvent {
readonly kind: "event";
readonly name: string;
readonly of?: Operand;
}
/** A session-calendar phase event: `phase open`. */
export interface PhaseEvent {
readonly kind: "phase";
readonly phase: string;
}
/** A wall-clock window: `time 09:30..10:00`, `time after 15:45`, `time before 10:00`.
* At least one of `from` / `to` must be present. */
export interface TimeWindowTrig {
readonly kind: "time-window";
readonly from?: TimeOfDay;
readonly to?: TimeOfDay;
}
/** A fill-lifecycle event (fill telemetry as a trigger). */
export interface FillEvent {
readonly kind: "fill";
readonly event: "filled" | "partial-fill" | "unfilled" | "rejected" | "cancelled";
readonly leg?: number;
}
export interface And {
readonly kind: "and";
readonly terms: readonly Trigger[];
}
export interface Or {
readonly kind: "or";
readonly terms: readonly Trigger[];
}
export interface Not {
readonly kind: "not";
readonly term: Trigger;
}
/** The shared WHEN expression, used by Wake, Plan, and Grade filters alike. */
export type Trigger =
| Comparison
| CrossEvent
| BreakHold
| WithinWindow
| UntilWindow
| AtWindow
| TimeHeldStop
| ClockStop
| NthEvent
| StructuralEvent
| PhaseEvent
| TimeWindowTrig
| FillEvent
| And
| Or
| Not;
// ─────────────────────────────────────────────────────────────────────────────
// Price expressions (CONTEXT / ARCHITECTURE: ExecutionFair, the @fair anchor)
// ─────────────────────────────────────────────────────────────────────────────
/** A price anchor. `fair` = ExecutionFair (the honest MM fill price); `mid` is an
* authoring resting-price, never a fair-value source (ARCHITECTURE §4). */
export type AnchorName =
| "fair"
| "intrinsic"
| "basis"
| "bid"
| "ask"
| "mid"
| "last"
| "join"
| "improve"
| "stub"
// The underlying spot as a PRICE anchor (emergent grammar, ADR-0030 / kestrel-ipc). `spot` is
// already a first-class SERIES name (`WHEN spot > 499`); admitting it in the `@ price` position
// makes the anchor set a strict superset — no previously-parsed form changes meaning. It resolves
// to the canonical underlying spot for an equity/spot leg and is REFUSED for an option leg
// (the mirror image of `intrinsic`/`basis`, which parse always but are refused on a spot leg —
// ADR-0017). The two readings of `spot` (series vs anchor) read the SAME underlying value.
| "spot";
export interface Anchor {
readonly kind: "anchor";
readonly name: AnchorName;
}
/** An absolute limit price, e.g. `4.20`. */
export interface AbsolutePrice {
readonly kind: "price-abs";
readonly value: number;
}
/** `base ± N(c|%)` — a tight offset from a base price, e.g. `fair-3c`. */
export interface PriceOffset {
readonly kind: "price-offset";
readonly base: PriceExpr;
readonly sign: "+" | "-";
readonly amount: number;
readonly unit: "c" | "%";
}
/** `lean(a, b, x)` — blend between two prices by fraction x (lean-to-MM). */
export interface Lean {
readonly kind: "lean";
readonly a: PriceExpr;
readonly b: PriceExpr;
readonly x: number;
}
/** `min(...)` / `max(...)` over price expressions. */
export interface PriceMinMax {
readonly kind: "price-fn";
readonly fn: "min" | "max";
readonly args: readonly PriceExpr[];
}
export type PriceExpr = Anchor | AbsolutePrice | PriceOffset | Lean | PriceMinMax;
/** One rung of an escalation ladder: escalate the resting price `to` after `after`. */
export interface EscStage {
readonly kind: "esc-stage";
readonly to: PriceExpr;
readonly after: Duration;
}
/** The resting-order execution policy: peg/fix, an esc ladder, cap/floor lists, a
* per-order cancel-if guard, and gtc. Distinct from *which* price (that is a
* {@link PriceExpr}). */
export interface OrderPolicy {
readonly kind: "order-policy";
readonly pricing?: "peg" | "fix";
readonly esc?: readonly EscStage[];
readonly caps?: readonly PriceExpr[];
readonly floors?: readonly PriceExpr[];
readonly cancelIf?: Trigger;
readonly gtc?: boolean;
}
// ─────────────────────────────────────────────────────────────────────────────
// Order legs (CONTEXT: Book; ADR-0005 Plan v1)
// ─────────────────────────────────────────────────────────────────────────────
/** How a strike is chosen: relative to the anchor grid (`+1`), an absolute strike
* (`450`), at-the-money (`atm`), or delta-targeted (`25d`). */
export type StrikeSpec =
| { readonly kind: "strike-rel"; readonly steps: number }
| { readonly kind: "strike-abs"; readonly strike: number }
| { readonly kind: "strike-atm" }
| { readonly kind: "strike-delta"; readonly delta: number };
/** One option leg: `buy 2 +1 C`.
*
* A SELL leg is legal ONLY as a **covered** close of inventory this plan actually holds — never-naked
* applies to OPTION legs exactly as it does to an {@link EquityLeg} (ADR-0017). Coverage is enforced
* per `(strike, right)` (`PlanEngine#sellCovered`: net-held-of-that-leg minus its resting sells) on each
* surface that can emit a sell: entry, ALSO, RELOAD, flatten, **TAKE-PROFIT**, and **EXIT**.
*
* The last two are called out because they were the hole (kestrel-h5nx). Both sized off the PLAN-TOTAL
* net-held while binding the sell to a SINGLE leg, so any plan holding more than one leg (a straddle, a
* two-strike ladder) oversold that leg and went NET SHORT an uncovered option. On a one-leg plan the two
* quantities coincide, which is why it survived for so long — and why a contaminated result had already
* been pinned into the committed golden fixtures. Multi-leg sells now ALLOCATE across the held legs, each
* capped by and named for the leg that covers it. */
export interface OptionLeg {
readonly kind: "leg";
readonly side: "buy" | "sell";
readonly qty: number;
readonly strike: StrikeSpec;
readonly right: "C" | "P";
/**
* The leg's own tenor (kestrel theta-cell seam a; AUTHORABLE since kestrel-ih5h seam 1).
*
* `undefined` means **inherit the ambient execution tenor** — the expiry authored once on the
* execution instrument (`USING exec … 0dte`, {@link ExpirySelector} on {@link Instrument}). It is
* never a silent 0dte: the inheritance is resolved at execution (`#execForLeg` in
* `src/engine/plans.ts`), and an expiry that cannot be resolved fails closed rather than defaulting.
*
* Authored per-leg with the `exp` marker — `buy 2 +1 C exp 0dte`, `sell 1 atm P exp 2026-07-17` —
* reusing the SAME selector vocabulary as the instrument form. The marker is required because a leg
* is followed by `,` or `@` and a bare tag selector would swallow the continuation; the instrument
* form can be positional only because a stop-set holds it apart. Legs written without `exp` print
* byte-identically to before this syntax existed (ADR-0004; the tail is additive).
*
* It is also what lets a leg RECONSTRUCTED from a held/marked position (not authored — e.g. the
* per-wake mark-to-model folding a held straddle back out of the tape's option book, whose
* `BookState` carries one expiry) carry its own tenor with it.
*
* Note the execution BOOK is still keyed by SYMBOL alone (`#books` in `src/engine/plans.ts`,
* `foldBook` in `src/bus/types.ts` keeps ONE expiry per instrument). A leg expiry therefore reaches
* execution as a CHECK, not a selector: `#bookFor` refuses a book whose expiry contradicts the
* authored one (fail-closed) rather than pricing against the wrong tenor. Re-keying the book by
* `(symbol, expiry)` — what a genuine multi-expiry / calendar position needs — is the other half of
* this seam and is deliberately NOT made here.
*/
readonly expiry?: ExpirySelector;
}
/** One equity/spot leg: `buy 100 shares` (ADR-0017). A spot instrument has neither a
* strike nor a right, so an equity leg carries only side + quantity + the `shares` marker;
* the tradeable symbol is the ambient `USING exec <SYMBOL>` with no expiry. `qty` is a share
* count. Deliberately long-biased in v1: an uncovered equity SELL is a naked short (unbounded
* risk) and is refused downstream (never-naked, ADR-0017). The SAME boundary governs an
* {@link OptionLeg} sell — never-naked is not an equity-only rule (kestrel-h5nx). */
export interface EquityLeg {
readonly kind: "equity-leg";
readonly side: "buy" | "sell";
readonly qty: number;
}
/** An order leg — an {@link OptionLeg} (`buy 2 +1 C`) or an {@link EquityLeg} (`buy 100
* shares`, ADR-0017). A discriminated union on `kind`; every consumer narrows, so the
* compiler enumerates the sites the instrument-general execution core must handle. */
export type Leg = OptionLeg | EquityLeg;
// ─────────────────────────────────────────────────────────────────────────────
// Plan clauses (ADR-0005: WHEN / DO / ALSO / RELOAD / TP / EXIT / INVALIDATE /
// CANCEL-IF / ARM; inventory binding)
// ─────────────────────────────────────────────────────────────────────────────
/** A held-inventory quantifier for clauses that act on pre-existing positions
* (ADR-0005 inventory binding). */
export type HeldQuantifier =
| { readonly kind: "held-foreach" } // FOREACH held leg
| { readonly kind: "held-any" }; // any held leg
/** `DO <legs> @ <price> <policy>` — the primary order ticket. `atomic` is reserved and
* refused symmetrically on every surface (ADR-0005): the parser REJECTS the keyword, the
* builders REFUSE to construct it, and the printer THROWS on it. The AST keeps the field
* anyway — the parser needs the vocabulary to reject it loudly, and the type documents the
* future surface (a whole-structure preflight + an atomic execution adapter) — but no valid
* v1 ticket ever carries it, so `parse(print(x))` round-trips (ADR-0004). Never faked. */
export interface DoTicket {
readonly kind: "do";
readonly legs: readonly Leg[];
readonly price: PriceExpr;
readonly policy?: OrderPolicy;
readonly atomic?: boolean;
}
/** `ALSO <legs> @ <price> <policy>` — an additional co-armed ticket. */
export interface AlsoTicket {
readonly kind: "also";
readonly legs: readonly Leg[];
readonly price: PriceExpr;
readonly policy?: OrderPolicy;
readonly atomic?: boolean;
}
/** `RELOAD [WHEN <trig>] <legs> @ <price> <policy>` — buy more on adverse movement. */
export interface ReloadClause {
readonly kind: "reload";
readonly when?: Trigger;
readonly legs: readonly Leg[];
readonly price: PriceExpr;
readonly policy?: OrderPolicy;
readonly atomic?: boolean;
}
/** What target a take-profit rests against. */
export type TpTarget =
| { readonly kind: "tp-pct"; readonly pct: number } // +100%
| { readonly kind: "tp-mult"; readonly mult: number } // 2x
| { readonly kind: "tp-price"; readonly price: PriceExpr };
/** `TP <target> [frac F] [<held-quant>] [@ <price>] <policy>` — resting take-profit. */
export interface TpClause {
readonly kind: "tp";
readonly target: TpTarget;
readonly frac?: number;
readonly over?: HeldQuantifier;
readonly price?: PriceExpr;
readonly policy?: OrderPolicy;
}
/** `EXIT <trig> [<held-quant>] [@ <price>] <policy>` — get out when the thesis breaks. */
export interface ExitClause {
readonly kind: "exit";
readonly when: Trigger;
readonly over?: HeldQuantifier;
readonly price?: PriceExpr;
readonly policy?: OrderPolicy;
}
/** `INVALIDATE <trig>` — thesis dead: stop active management, ride the tail. */
export interface InvalidateClause {
readonly kind: "invalidate";
readonly when: Trigger;
}
/** `CANCEL-IF <trig>` — cancel resting orders (plan-scope, vs the per-order guard). */
export interface CancelIfClause {
readonly kind: "cancel-if";
readonly when: Trigger;
}
/** `ARM [WHEN <trig>] [basis <price>] [<held-quant>]` — the on-arm trigger + inventory
* binding (ADR-0005): the basis price anchor and a held-leg quantifier. */
export interface ArmClause {
readonly kind: "arm";
readonly when?: Trigger;
readonly basis?: PriceExpr;
readonly over?: HeldQuantifier;
}
export type PlanClause =
| DoTicket
| AlsoTicket
| ReloadClause
| TpClause
| ExitClause
| InvalidateClause
| CancelIfClause
| ArmClause;
// ─────────────────────────────────────────────────────────────────────────────
// Instruments, USING, provenance
// ─────────────────────────────────────────────────────────────────────────────
/** An expiry selector for an execution instrument. */
export type ExpirySelector =
| { readonly kind: "expiry-dte"; readonly dte: number } // 0dte, 1dte
| { readonly kind: "expiry-date"; readonly date: string } // 2026-07-17
| { readonly kind: "expiry-tag"; readonly tag: string }; // weekly, monthly
/** A tradeable instrument, optionally with an expiry (execution side). */
export interface Instrument {
readonly kind: "instrument";
readonly symbol: string;
readonly expiry?: ExpirySelector;
}
/** Scoped defaults: a signal-space instrument (where WHEN reads) and an execution-space
* instrument + expiry (where DO acts). The only v1 customization point for imports
* (ADR-0003). */
export interface Using {
readonly kind: "using";
readonly signal?: Instrument;
readonly exec?: Instrument;
}
export type ProvenanceTier = "vetted" | "candidate" | "unvetted";
/** Authorship + trust metadata. A declared provenance can only *narrow* the channel's
* authority (ARCHITECTURE §6 provenance ceiling). */
export interface Provenance {
readonly kind: "provenance";
readonly tier?: ProvenanceTier;
readonly author?: string;
readonly origin?: string;
readonly replay?: string;
}
// ─────────────────────────────────────────────────────────────────────────────
// VIEW (CONTEXT: View, Pane)
// ─────────────────────────────────────────────────────────────────────────────
/** A pane argument, carrying only its **syntactic supersort** — the class the lexer/parser assigns
* with ZERO catalog knowledge (ADR-0041 §1: the `wf` rung is catalog-independent, so the parser
* never picks a pane's semantic sort). Five supersorts: a bare **ident** (`skyline`, `vwap`), a
* **window** (`5m` — a numeral + a time unit), a bare **numeral count** (`12` — a numeral with
* NO unit, e.g. `chain 12`), a **session ordinal** (`d-0`, `d-1` — the `d-` prefix + a numeral,
* Train 1B), and an **expiry ordinal** (`e-0`, `e-1` — the `e-` prefix + a numeral, Train 1B). A
* catalog `ParamSlot` REFINES a supersort to a semantic sort
* (`Instrument`/`LevelName`/`Window`/`Count`/`SessionOrdinal`/`ExpiryOrdinal`) at materialization
* (`src/frame/pane-catalog.ts`). Each supersort has ONE canonical printed form so `print(parse(text))`
* stays byte-stable (ADR-0004): an ident prints its name, a window `<n><unit>`, a count its bare
* numeral, a session ordinal `d-<n>`, an expiry ordinal `e-<n>`. Panes are references + args only —
* a View never computes (no application node, ADR-0041 §1). */
export type PaneArg =
| { readonly kind: "arg-ident"; readonly name: string }
| { readonly kind: "arg-window"; readonly window: Window }
| { readonly kind: "arg-count"; readonly count: number }
| { readonly kind: "arg-ordinal"; readonly ordinal: number }
| { readonly kind: "arg-expiry"; readonly expiry: number };
/** One named block of a View. */
export interface PaneRef {
readonly kind: "pane";
readonly name: string;
readonly args: readonly PaneArg[];
}
export interface ViewStatement {
readonly kind: "view";
readonly name: string;
readonly budget?: number; // token budget
readonly panes: readonly PaneRef[];
/** Byte-stable comment trivia (ADR-0033). Absent on comment-free documents. */
readonly comments?: CommentLayer;
}
// ─────────────────────────────────────────────────────────────────────────────
// Citation — the `because` pre-registration clause (kestrel-rtf)
// ─────────────────────────────────────────────────────────────────────────────
/**
* A content-hash reference to a platform-side Thesis artifact — the `because` clause
* (kestrel-rtf). It carries ONLY the digest of the fixed shape `sha256:<64 hex>`; the thesis
* body lives platform-side, never inline (a `because` with an inline body is a parse refusal).
*
* Because the clause prints byte-stably like any other canonical text, it binds into the
* armed-document hash (`sha256(print(module))`) automatically — so pre-registration is
* tamper-evident once the plan is armed. It is an OPTIONAL field on {@link PlanStatement} and
* {@link WakeStatement}, NOT a fifth statement kind: the statement algebra stays View/Wake/
* Plan/Grade (ADR-0001), and `because` attached to a View or Grade is a fail-closed refusal.
*/
export interface Citation {
readonly kind: "citation";
readonly algo: "sha256";
/** The 64-lowercase-hex content hash of the platform-side Thesis (no inline body). */
readonly hash: string;
}
// ─────────────────────────────────────────────────────────────────────────────
// WAKE (CONTEXT: Wake, Scan, Wake delivery)
// ─────────────────────────────────────────────────────────────────────────────
/** `DELIVER <view> [MANDATORY] [KEYFRAME]` — the wake decides *when*, the View *what*.
* Delta frame by default; keyframe on request. */
export interface DeliverSpec {
readonly kind: "deliver";
readonly view: string;
readonly mandatory?: boolean;
readonly keyframe?: boolean;
}
/** A wake's attention budget: wakes/day and/or tokens/day. */
export interface WakeBudget {
readonly kind: "wake-budget";
readonly wakesPerDay?: number;
readonly tokensPerDay?: number;
}
/** A Scan's universe scope (`all-listed`, `nyse`) — wide and slow, vs a narrow coverage
* wake. */
export interface UniverseScope {
readonly kind: "universe";
readonly universe: string;
}
export interface WakeStatement {
readonly kind: "wake";
readonly name: string;
readonly when: Trigger;
/** Optional `because sha256:<64 hex>` pre-registration citation (kestrel-rtf). */
readonly because?: Citation;
readonly deliver?: DeliverSpec;
readonly priority?: number;
readonly coalesce?: string;
readonly budget?: WakeBudget;
readonly universe?: UniverseScope;
/** Byte-stable comment trivia (ADR-0033). Absent on comment-free documents. */
readonly comments?: CommentLayer;
}
// ─────────────────────────────────────────────────────────────────────────────
// PLAN (ARCHITECTURE §2; ADR-0005)
// ─────────────────────────────────────────────────────────────────────────────
/** Budget as a risk fraction, `0.2R`. `size × max_loss ≤ budget` (ARCHITECTURE §6). */
export interface Budget {
readonly kind: "budget";
readonly value: number;
readonly unit: "R";
}
/** Time-to-live: relative (`+30m`) or an absolute clock (`16:00`, hold-to-close). */
export type Ttl =
| { readonly kind: "ttl-rel"; readonly dur: Duration }
| { readonly kind: "ttl-at"; readonly at: TimeOfDay };
/** One tag binding of a regime gate: `intraday: trend`. */
export interface RegimeTagBinding {
readonly scope: string;
readonly value: string;
}
/** `regime {intraday: trend}` — an open-vocabulary tag gate (ADR-0005). */
export interface RegimeGate {
readonly kind: "regime-gate";
readonly tags: readonly RegimeTagBinding[];
}
/** The lifecycle standing of a standing statement (CONTEXT: Standing). */
export type Standing = "authored" | "armed" | "versioned" | "superseded";
export interface PlanStatement {
readonly kind: "plan";
readonly name: string;
readonly budget?: Budget;
readonly ttl?: Ttl;
readonly regime?: RegimeGate;
readonly priority?: number;
readonly standing?: Standing;
readonly provenance?: Provenance;
readonly using?: Using;
readonly when?: Trigger;
/** Optional `because sha256:<64 hex>` pre-registration citation (kestrel-rtf). */
readonly because?: Citation;
readonly clauses: readonly PlanClause[];
readonly atomic?: boolean;
/** Byte-stable comment trivia (ADR-0033). Absent on comment-free documents. */
readonly comments?: CommentLayer;
}
// ─────────────────────────────────────────────────────────────────────────────
// GRADE (ADR-0006)
// ─────────────────────────────────────────────────────────────────────────────
/** What is being graded — everything authored is gradable (ADR-0006). */
export interface GradeSubject {
readonly kind: "grade-subject";
readonly what: "plan" | "wake" | "view" | "pod" | "tag";
readonly name: string;
}
/** A counterfactual — syntax, not a bespoke script (ADR-0006). The structural null and
* bracket cross the SAME fill model. */
export type Counterfactual =
| { readonly kind: "cf-ungated" }
| { readonly kind: "cf-null" }
| { readonly kind: "cf-bracket" };
/** The corpus range a grade replays over: `2025-01..2026-06`. Opaque date tokens. */
export interface CorpusRange {
readonly kind: "corpus-range";
readonly from: string;
readonly to: string;
}
/** A `BY` stratification dimension — cells, not pools (ADR-0006). */
export type GradeDimension =
| { readonly kind: "dim-series"; readonly series: SeriesRef }
| { readonly kind: "dim-vehicle" }
| { readonly kind: "dim-name" }
| { readonly kind: "dim-lineage" };
export interface GradeStatement {
readonly kind: "grade";
readonly subject: GradeSubject;
readonly over?: CorpusRange;
readonly fill?: string; // named, versioned fill model
readonly versus: readonly Counterfactual[];
readonly by: readonly GradeDimension[];
/** Byte-stable comment trivia (ADR-0033). Absent on comment-free documents. */
readonly comments?: CommentLayer;
}
// ─────────────────────────────────────────────────────────────────────────────
// POD / BOOK / RISK (ADR-0002)
// ─────────────────────────────────────────────────────────────────────────────
/** Instruments assigned to a Trader PLUS the thesis for why (CONTEXT: Coverage). */
export interface Coverage {
readonly kind: "coverage";
readonly instruments: readonly Instrument[];
readonly thesis: string;
}
/** How a Book's concurrent plans compete for its envelope (open-vocabulary policy). */
export interface Arbitration {
readonly kind: "arbitration";
readonly policy: string;
readonly concurrency?: number;
}
/** One risk line: `RISK day-loss 2R -> halt`. The L0 envelope may clamp/veto, never open
* risk (ARCHITECTURE §3). */
export interface RiskLine {
readonly kind: "risk";
readonly metric: string;
readonly threshold: Quantity;
readonly action: string;
}
/** The leaf of the org tree — the only place positions and orders live (ADR-0002). */
export interface BookStatement {
readonly kind: "book";
readonly name: string;
readonly budget?: Budget;
readonly coverage?: Coverage;
readonly arbitration?: Arbitration;
readonly risk?: readonly RiskLine[];
/** Byte-stable comment trivia (ADR-0033). Absent on comment-free documents. */
readonly comments?: CommentLayer;
}
/** The recursive org node: an allocating PM role + a risk envelope + children (each a
* Book or another Pod). Depth is unbounded; the org is data (ADR-0002). */
export interface PodStatement {
readonly kind: "pod";
readonly name: string;
readonly risk: readonly RiskLine[];
readonly children: readonly (PodStatement | BookStatement)[];
/** Byte-stable comment trivia (ADR-0033). Absent on comment-free documents. */
readonly comments?: CommentLayer;
}
// ─────────────────────────────────────────────────────────────────────────────
// Module / Document (ADR-0003)
// ─────────────────────────────────────────────────────────────────────────────
/** An ESM-like import of named statements from another module (ADR-0003). */
export interface ImportDecl {
readonly kind: "import";
readonly names: readonly string[];
readonly from: string;
}
/** Any top-level statement of a module. */
export type Statement =
| ViewStatement
| WakeStatement
| PlanStatement
| GradeStatement
| PodStatement
| BookStatement;
/** A Kestrel document: imports + optional USING defaults + statements + provenance. */
export interface Module {
readonly kind: "module";
readonly imports: readonly ImportDecl[];
readonly using?: Using;
readonly statements: readonly Statement[];
readonly provenance?: Provenance;
/** Byte-stable comment trivia (ADR-0033) for the module's own directive lines (IMPORT /
* PROVENANCE / USING, ordinal 0..k-1) and its tail. Comments before/on a statement live on
* that statement, not here. Absent on comment-free documents. */
readonly comments?: CommentLayer;
}
/** Any node the printer accepts at the top level. */
export type KestrelNode = Module | Statement;