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.

182 lines (181 loc) 10.7 kB
/** * # frame/refusals — the CLOSED refusal-constructor union for the inflection path (ADR-0041 §1/§3) * * Every {@link KernelHonestyError} the inflection path throws is now a TYPED constructor here, one * per refusal kind. The message is GENERATED from the constructor — never hand-written at a throw * site — so each refusal kind has ONE canonical printed form (golden-tested in * `tests/frame.refusal-constructors.test.ts`), and "refused" is never one undifferentiated * condition: every message names the rung it failed on (ADR-0041 §3's judgment ladder) plus the * pane / slot / sort / value the fix must address. * * The canonical forms are BYTE-IDENTICAL to Train 1A's inline strings ({@link ./pane-catalog.ts}, * kestrel-wa0j.24) — this train relocates the message construction into the constructor without * changing a byte, so no existing refusal fixture re-pins. * * Cell-state citations (ADR-0041 §3): a LATENT refusal names its train, PULLED FROM THE LEDGER * ({@link ./paradigm-ledger.ts} — the single source, so the burn-down and the refusal quote the same * string); a DEFECTIVE refusal names its written reason + its sanctioned periphrasis. * * Import discipline: this module imports the arg/slot TYPES from `./pane-catalog.ts` type-only (erased * at runtime), and `./pane-catalog.ts` imports {@link paneRefusal} as a value — so the only runtime * edge is pane-catalog → refusals (no cycle). */ import { KernelHonestyError } from "./types.js"; import { latentTrainFor } from "./paradigm-ledger.js"; // ── local, frozen mirrors of the arg-printing helpers (kept here so this module has no runtime // dependency on pane-catalog — the map/print logic is trivial and semantically pinned) ────────── /** Print one pane arg (`5m`, `skyline`, `12`, `d-0`, `e-1`) — mirrors the language print / canonical form. */ function argStr(a) { if (a.kind === "arg-ident") return a.name; if (a.kind === "arg-count") return `${a.count}`; if (a.kind === "arg-ordinal") return `d-${a.ordinal}`; if (a.kind === "arg-expiry") return `e-${a.expiry}`; return `${a.window.value}${a.window.unit}`; } /** Join a run of args for a message (`"5m vwap"`), byte-identical to `args.map(paneArgStr).join(" ")`. */ function argsStr(args) { return args.map(argStr).join(" "); } /** The syntactic supersort the parser assigned an arg (mirrors `pane-catalog.syntacticSupersort`). */ function syntacticSupersort(a) { switch (a.kind) { case "arg-ident": return "ident"; case "arg-count": return "numeral"; case "arg-ordinal": return "ordinal"; case "arg-expiry": return "expiry"; case "arg-window": return "window"; } } /** The syntactic supersort each semantic sort refines FROM (mirrors `pane-catalog.SORT_ACCEPTS`). */ const SORT_ACCEPTS = { Instrument: "ident", LevelName: "ident", Window: "window", Count: "numeral", SessionOrdinal: "ordinal", ExpiryOrdinal: "expiry", }; /** Repair-guiding phrase for a supersort (mirrors `pane-catalog.supersortPhrase`). */ function supersortPhrase(s) { switch (s) { case "ident": return "a bare ident"; case "window": return "a window (a numeral + a time unit, e.g. `5m`)"; case "numeral": return "a bare count numeral (e.g. `12`)"; case "ordinal": return "a session ordinal (`d-<n>`, e.g. `d-0`)"; case "expiry": return "an expiry ordinal (`e-<n>`, e.g. `e-0`)"; } } /** The `name:sort…` spec of a signature (mirrors the inline `slotSpec` in `pane-catalog.bindArgs`). */ function slotSpecOf(slots) { return slots.map((s) => `${s.name}:${s.sort}${s.arity === "many" ? "…" : ""}`).join(", "); } /** The refusal kinds, as a closed roster (for exhaustiveness tests). */ export const PANE_REFUSAL_KINDS = [ "unknown-pane", "takes-no-args", "no-signature", "arity", "sort-mismatch", "count-not-integer", "ordinal-not-nonneg-integer", "window-not-servable", "unknown-value-naming-roster", "latent-cell", "defective-cell", "over-budget", "unreachable-bound-arg", "unreachable-not-catalogued", "unreachable-args-on-argless", ]; /** * Construct the ONE canonical {@link KernelHonestyError} for a refusal (ADR-0041 §1/§3). The message * is generated HERE from `r`'s fields — never hand-written at the throw site. Exhaustive over the * closed {@link PaneRefusal} union. */ export function paneRefusal(r) { return new KernelHonestyError(refusalMessage(r)); } function refusalMessage(r) { switch (r.kind) { case "unknown-pane": return (`View "${r.view}" names pane "${r.pane}", which is NOT in the pane catalog — the pane cannot ` + `render (single-source: a View selects only catalogued panes). Known panes: ` + `${r.known.join(", ")} (fail-closed, never a silent drop).`); case "takes-no-args": return (`View "${r.view}" passes arg${r.args.length === 1 ? "" : "s"} "${argsStr(r.args)}" ` + `to pane "${r.pane}", which takes no arguments — an arg a pane does not understand is refused ` + `(fail-closed, never silently ignored).`); case "no-signature": return (`pane "${r.pane}" received arg${r.args.length === 1 ? "" : "s"} "${argsStr(r.args)}" but declares ` + `no ParamSlot signature — cannot elaborate its args (fail-closed at the \`mat\` rung; a catalog defect).`); case "arity": return (`pane "${r.pane}" was served ${r.args.length} arg${r.args.length === 1 ? "" : "s"} "${argsStr(r.args)}" ` + `but its signature binds only [${slotSpecOf(r.slots)}] — too many arguments. Judgment REFUSED at the \`mat\` rung ` + `(materializable: an argument must elaborate at its slot's sort and arity, ADR-0041 §3; fail-closed, never silently dropped).`); case "sort-mismatch": { const want = SORT_ACCEPTS[r.slot.sort]; const got = syntacticSupersort(r.arg); return (`pane "${r.pane}" · slot "${r.slot.name}" (sort ${r.slot.sort}): expected ${supersortPhrase(want)} but was served ` + `${supersortPhrase(got)} "${argStr(r.arg)}". Judgment REFUSED at the \`mat\` rung (materializable: an argument ` + `must elaborate at its slot's sort, ADR-0041 §3; fail-closed, never a silent coercion).`); } case "count-not-integer": return (`pane "${r.pane}" · slot "${r.slot.name}" (sort Count): a Count must be a positive integer, got "${argStr(r.arg)}". ` + `Judgment REFUSED at the \`mat\` rung (a cardinal names N nearest-money addresses; fail-closed, never a fractional count).`); case "ordinal-not-nonneg-integer": return (`pane "${r.pane}" · slot "${r.slot.name}" (sort ${r.slot.sort}): an ordinal must be a non-negative integer, got "${argStr(r.arg)}". ` + `Judgment REFUSED at the \`mat\` rung (a ${r.slot.sort} names an ordinal into the declared boundary stream — ` + `${r.slot.sort === "SessionOrdinal" ? "d-0 the current session, d-1 the prior" : "e-0 the nearest expiry, e-1 the next out"}; ` + `fail-closed, never a fractional or negative ordinal).`); case "window-not-servable": return (`pane "${r.pane}" cannot serve window arg "${argStr(r.arg)}": the tape rows are served pre-bucketed at ` + `${r.servedBucketMin}m — a renderable window must be a multiple of the served ${r.servedBucketMin}m (a sub-resolution or ` + `non-multiple window cannot be expressed by these rows; rendering them under a "${argStr(r.arg)}" ` + `label would misstate the scale — fail-closed, never mismatched data as if requested).`); case "unknown-value-naming-roster": return (`pane "${r.pane}" · slot "${r.slot}": "${r.value}" is not a known level — the levels roster is ` + `{${r.roster.join(", ")}}. Judgment REFUSED (an ident must name a cell the LevelSet carries; ` + `fail-closed, never a silent drop). Cell state: a named level-SET like \`registry\` is LATENT — the ` + `band-keyed level-set inflection (a later train), NOT Train 1A's individual-level idents (ADR-0041 §3).`); case "latent-cell": { const train = latentTrainFor(r.pane); const named = train ?? "(no train recorded in the ledger — a ledger gap, fail-closed)"; return (`pane "${r.pane}" · form "${r.form}": the paradigm cell is LATENT — not yet materializable, but a ` + `named train will land it. Deferred train: ${named}. Judgment REFUSED at the \`mat\` rung ` + `(ADR-0041 §3: a LATENT refusal names its train; fail-closed, never a silent drop).`); } case "defective-cell": { const cond = r.conditioning !== undefined ? ` (conditioned on ${r.conditioning})` : ""; return (`pane "${r.pane}" · form "${r.form}": the paradigm cell is DEFECTIVE — it never materializes${cond}. ` + `Written reason: ${r.reason}. Sanctioned periphrasis: ${r.periphrasis}. Judgment REFUSED ` + `(ADR-0041 §3: a DEFECTIVE refusal names its written reason + its sanctioned periphrasis; ` + `absent-not-hidden — the address has no value under this paradigm, and the periphrasis renders in its place, never an invented one).`); } case "over-budget": return (`View "${r.view}" is over its token budget: the selected panes (${r.paneIds.join(", ")}) ` + `measured ${r.spent} tokens under tokenizer "${r.tokenizer}", exceeding the budget of ${r.budget}. ` + "Refusing to render a View that does not fit its own budget (fail-closed — trim the View or raise its budget; " + "the non-configurable kernel is not counted against the View budget)."); case "unreachable-bound-arg": return `pane "${r.pane}" expected ${r.expected} but was reached with "${argsStr(r.args)}" (fail-closed).`; case "unreachable-not-catalogued": return `pane "${r.pane}" is not in the catalog — cannot materialize (fail-closed).`; case "unreachable-args-on-argless": return (`pane "${r.pane}" takes no arguments but was resolved with "${argsStr(r.args)}" ` + `— cannot materialize (fail-closed).`); } }