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.
134 lines (133 loc) • 8.37 kB
JavaScript
/**
* # session/armed-plan-terms — capture the ARMED plans' enforced terms for the watcher (kestrel-wa0j.29)
*
* The watcher is asked to judge a plan's PREMISE ("the plan armed on a thesis — is it still true?") yet
* it NEVER sees the armed document's text: the WHEN/DO/TP/EXIT/INVALIDATE clauses live in the supersede
* action on the bus and are never rendered into any frame — the kernel carries only the plan's lifecycle
* NAME + STATE (frame/types.ts PlanStateEntry). This module closes that gap by capturing, from the armed
* document ASTs the driver ALREADY holds, the TYPED terms the `armed-plan` pane renders.
*
* ## Attribution honesty (ADR-0041 §1: a pane invents no value)
* Every term is a TYPED value from the plan AST printed through the SAME canonical printer the
* round-trip is defined by ({@link ../lang/print.ts planClause}/{@link ../lang/print.ts printTrigger}/
* {@link ../lang/print.ts budgetStr}) — a typed value → its byte-stable text, NEVER a hand-rolled
* restatement, never the rendered pane bytes. The data enters the frame THROUGH the frame input, exactly
* as the `delta`/`prior-session` panes' data does.
*
* ## Which plans (absent-not-hidden, ADR-0041 §3)
* ONLY plans in an ENFORCED lifecycle state — `armed` / `fired` / `managing` — carry terms the watcher
* is actively enforcing; an `authored` (not yet armed) or `done` plan is excluded. An empty enforced set
* is threaded as ABSENT ({@link armedPlanTermsOf} returns `undefined`), never an empty list — so the pane
* fails closed to exactly one absent-with-reason line, byte-identical to a frame without the field.
*
* Pure: no clock, no RNG. A same-name replacement's LATEST armed AST wins (the driver's arm order).
*/
import { budgetStr, planClause, printTrigger } from "../lang/index.js";
/** The plan lifecycle states in which the watcher is ENFORCING the plan's terms (kestrel-wa0j.29) — the
* plan has committed (or is committing) capital and the WHEN/TP/EXIT/INVALIDATE are live. An `authored`
* plan (awaiting its arm gate) and a `done` plan (terminal) carry no terms the watcher enforces NOW. */
export const ENFORCED_PLAN_STATES = new Set(["armed", "fired", "managing"]);
/**
* Calendar days from the tape's `session_date` to an authored per-leg expiry DATE (kestrel-wa0j.74) —
* the SAME days-to-expiry the ih5h date-blind chain layer serves as `+Nd`
* ({@link ../session/simulate.ts dateBlindOptions}), computed here from the session date the grammar's
* `dte` selector is itself defined against ("`0dte` means expiring on the session's date",
* {@link ../engine/plans.ts expiryAdmits}). Both dates are ISO `YYYY-MM-DD`; the difference is a
* whole-day count taken at UTC midnight, so it is DST-free and deterministic (no wall clock).
*/
function dteFromSessionDate(sessionDate, expiryDate) {
const utcMidnight = (d) => {
const [y, m, day] = d.split("-").map((s) => Number.parseInt(s, 10));
return Date.UTC(y ?? 0, (m ?? 1) - 1, day ?? 1);
};
return Math.round((utcMidnight(expiryDate) - utcMidnight(sessionDate)) / 86_400_000);
}
/**
* Date-blind ONE expiry selector for the armed-plan pane (kestrel-wa0j.74). A relative `expiry-dte`
* (`0dte`) or a `expiry-tag` (`weekly`) is ALREADY date-blind and passes through untouched; an ABSOLUTE
* `expiry-date` (`2026-08-21`, authorable since kestrel-ih5h seam 1) is normalized to its DTE-relative
* `expiry-dte` equivalent against the session date. The canonical printer then emits the grammar's OWN
* `<n>dte` form, so NO iso-date token crosses the agent boundary and the {@link
* ../session/simulate.ts assertFrameDateBlind} fence stays green. print.ts is untouched — the authored
* document still prints its real date (round-trip stability, ADR-0004); only this FRAME capture blinds it.
*/
function dateBlindExpiry(e, sessionDate) {
return e.kind === "expiry-date" ? { kind: "expiry-dte", dte: dteFromSessionDate(sessionDate, e.date) } : e;
}
/** Date-blind an order leg's per-leg tenor. Only an OPTION leg (`kind: "leg"`) carries an `exp`; an
* equity leg and an expiry-less option leg pass through unchanged. */
function dateBlindLeg(l, sessionDate) {
return l.kind === "leg" && l.expiry !== undefined ? { ...l, expiry: dateBlindExpiry(l.expiry, sessionDate) } : l;
}
/** Date-blind the per-leg tenors of a clause before it is printed into the pane. Only `do`/`also`/`reload`
* carry legs (the entry tickets); every other clause passes through unchanged. */
function dateBlindClause(c, sessionDate) {
if (c.kind === "do" || c.kind === "also" || c.kind === "reload") {
return { ...c, legs: c.legs.map((l) => dateBlindLeg(l, sessionDate)) };
}
return c;
}
/** The ENTRY tickets — what the plan acquires/reloads. */
function isEntry(c) {
return c.kind === "do" || c.kind === "also" || c.kind === "reload";
}
/** The EXIT surface — take-profits + thesis-break stops the watcher enforces to get OUT. */
function isExit(c) {
return c.kind === "tp" || c.kind === "exit";
}
/** The INVALIDATION surface — thesis-dead + resting-cancels that kill the thesis. */
function isInvalidation(c) {
return c.kind === "invalidate" || c.kind === "cancel-if";
}
/** Capture ONE armed plan's enforced terms as a typed {@link ArmedPlanTermsEntry}: the arming premise
* (top-level `WHEN`), the entry/exit/invalidation clause runs, and the size envelope (`budget`) — each
* printed through the canonical printer, each optional-honest (an absent field is a clause the plan does
* not have, NEVER a fabricated one). Per-leg expiries are date-blinded against `sessionDate` FIRST
* ({@link dateBlindClause}, kestrel-wa0j.74) so an absolute `exp 2026-08-21` renders as a relative
* `<n>dte` and no iso-date crosses the agent boundary. Pure. */
export function armedPlanEntryOf(p, sessionDate) {
const when = p.when !== undefined ? printTrigger(p.when) : undefined;
const entries = p.clauses.filter(isEntry).map((c) => planClause(dateBlindClause(c, sessionDate)));
const exits = p.clauses.filter(isExit).map((c) => planClause(dateBlindClause(c, sessionDate)));
const invalidations = p.clauses.filter(isInvalidation).map((c) => planClause(dateBlindClause(c, sessionDate)));
const sizeEnvelope = p.budget !== undefined ? budgetStr(p.budget) : undefined;
return {
name: p.name,
...(when !== undefined ? { when } : {}),
...(entries.length > 0 ? { entries } : {}),
...(exits.length > 0 ? { exits } : {}),
...(invalidations.length > 0 ? { invalidations } : {}),
...(sizeEnvelope !== undefined ? { sizeEnvelope } : {}),
};
}
/**
* Capture the ARMED plans' enforced terms from the armed document ASTs, filtered to the plans whose
* CURRENT lifecycle state (per the frame's kernel PlanStateEntry set) is enforced — or `undefined` when
* NONE is enforced (absent-not-hidden: the pane then fails closed to absent-with-reason, and a frame
* without the field is byte-identical). Preserves the driver's arm order; a same-name replacement's
* LATEST AST wins (the last-armed document is the one whose terms are live). Pure.
*
* @param asts every armed plan statement the driver holds, in arm order.
* @param enforcedNames the plan NAMES currently in an enforced lifecycle state (from the kernel plan set).
* @param sessionDate the tape's `session_date` — the reference an absolute per-leg `exp` is blinded to
* a relative `<n>dte` against (kestrel-wa0j.74), so no iso-date reaches the frame.
*/
export function armedPlanTermsOf(asts, enforcedNames, sessionDate) {
// Latest-armed AST per name (a legal same-name replacement supersedes the prior terms).
const latestByName = new Map();
for (const p of asts)
latestByName.set(p.name, p);
const plans = [];
const seen = new Set();
for (const p of asts) {
if (seen.has(p.name))
continue; // one entry per name, in first-armed order (value is the latest AST)
if (!enforcedNames.has(p.name))
continue; // only plans the watcher is actively enforcing
seen.add(p.name);
plans.push(armedPlanEntryOf(latestByName.get(p.name), sessionDate));
}
if (plans.length === 0)
return undefined; // absent-not-hidden — never an empty list
return { plans };
}