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
JavaScript
/**
* # 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).`);
}
}