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.
1,547 lines (1,541 loc) • 104 kB
JavaScript
import {
__export
} from "./bin-wckvcay0.js";
// src/protocol/attestation.ts
var GRADE_SIGN_PREFIX = "kgrade1";
var ROOT_HASH_PREFIX = "sha256:";
var VERIFY_KEYS_PATH = "/.well-known/kestrel-markets";
var SIGNATURE_COVERED_GRADE_FIELDS = Object.freeze([
"kind",
"result",
"evView",
"supportPartition",
"pinnedDataRef",
"pinnedProvenanceRef",
"pinnedAuthorProvenanceRef",
"pinnedFillModelVersion",
"judge",
"issuedAt",
"boundRoots",
"counterfactual",
"kid",
"epoch",
"replayable"
]);
function canonicalize(value) {
if (value === null)
return null;
if (Array.isArray(value))
return value.map(canonicalize);
if (typeof value === "object") {
const src = value;
const out = {};
for (const key of Object.keys(src).sort()) {
const v = src[key];
if (v === undefined)
continue;
out[key] = canonicalize(v);
}
return out;
}
return value;
}
function canonicalizeJson(value) {
return JSON.stringify(canonicalize(value));
}
function certifiedPayload(fields) {
return {
kind: fields.kind,
result: fields.result,
evView: fields.evView,
supportPartition: fields.supportPartition,
pinnedDataRef: fields.pinnedDataRef,
...fields.pinnedProvenanceRef !== undefined ? { pinnedProvenanceRef: fields.pinnedProvenanceRef } : {},
...fields.pinnedAuthorProvenanceRef !== undefined ? { pinnedAuthorProvenanceRef: fields.pinnedAuthorProvenanceRef } : {},
pinnedFillModelVersion: fields.pinnedFillModelVersion,
judge: fields.judge,
issuedAt: fields.issuedAt,
boundRoots: [...fields.boundRoots],
...fields.counterfactual !== undefined ? { counterfactual: fields.counterfactual } : {},
kid: fields.kid,
epoch: fields.epoch,
replayable: fields.replayable
};
}
async function sha256Hex(text) {
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
const bytes = new Uint8Array(digest);
let hex = "";
for (const byte of bytes)
hex += byte.toString(16).padStart(2, "0");
return hex;
}
async function certifiedRoot(fields) {
return sha256Hex(canonicalizeJson(certifiedPayload(fields)));
}
// src/lang/lex.ts
class KestrelParseError extends Error {
line;
col;
name = "KestrelParseError";
constructor(message, line, col) {
super(`${message} (line ${line}, col ${col})`);
this.line = line;
this.col = col;
}
}
var PUNCT2 = ["..", "->", ">=", "<=", "==", "!="];
var PUNCT1 = [">", "<", "+", "-", "%", ":", ",", ".", "(", ")", "{", "}", "@", "/"];
function isDigit(ch) {
return ch >= "0" && ch <= "9";
}
function isWordStart(ch) {
return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "_";
}
function isWordPart(ch) {
return isWordStart(ch) || isDigit(ch);
}
function tokenizeLine(content, lineNo) {
const toks = [];
let i = 0;
const n = content.length;
while (i < n) {
const ch = content[i];
if (ch === " " || ch === "\t") {
i++;
continue;
}
if (ch === "#")
return { tokens: toks, trailing: content.slice(i + 1) };
const col = i + 1;
if (ch === '"') {
let j = i + 1;
let out = "";
while (j < n) {
const c = content[j];
if (c === "\\") {
const nx = content[j + 1];
if (nx === undefined) {
throw new KestrelParseError("unterminated string escape", lineNo, j + 1);
}
out += nx;
j += 2;
continue;
}
if (c === '"')
break;
out += c;
j++;
}
if (j >= n || content[j] !== '"') {
throw new KestrelParseError('unterminated string: expected a closing "', lineNo, col);
}
j++;
toks.push({ type: "string", text: out, line: lineNo, col, end: j + 1 });
i = j;
continue;
}
if (isDigit(ch)) {
let j = i + 1;
while (j < n && isDigit(content[j]))
j++;
if (content[j] === "." && j + 1 < n && isDigit(content[j + 1])) {
j += 2;
while (j < n && isDigit(content[j]))
j++;
}
toks.push({ type: "number", text: content.slice(i, j), line: lineNo, col, end: j + 1 });
i = j;
continue;
}
if (isWordStart(ch)) {
let j = i + 1;
while (j < n) {
const c = content[j];
if (isWordPart(c)) {
j++;
continue;
}
if (c === "-" && j + 1 < n && isWordStart(content[j + 1])) {
j += 2;
continue;
}
break;
}
toks.push({ type: "word", text: content.slice(i, j), line: lineNo, col, end: j + 1 });
i = j;
continue;
}
const two = content.slice(i, i + 2);
if (PUNCT2.includes(two)) {
toks.push({ type: "punct", text: two, line: lineNo, col, end: i + 3 });
i += 2;
continue;
}
if (PUNCT1.includes(ch)) {
toks.push({ type: "punct", text: ch, line: lineNo, col, end: i + 2 });
i++;
continue;
}
throw new KestrelParseError(`unexpected character ${JSON.stringify(ch)}`, lineNo, col);
}
return { tokens: toks };
}
function leadingIndent(content, lineNo) {
let i = 0;
while (i < content.length && content[i] === " ")
i++;
if (content[i] === "\t") {
throw new KestrelParseError("tab in indentation: Kestrel indents with spaces", lineNo, i + 1);
}
return i;
}
function lex(src) {
const physical = src.split(`
`);
const roots = [];
const stack = [];
let pending = [];
const moduleTail = [];
const attachTail = (c) => {
for (let s = stack.length - 1;s >= 0; s--) {
const node = stack[s];
if (node.indent < c.indent) {
node.tail = [...node.tail ?? [], c.text];
return;
}
}
moduleTail.push(c.text);
};
for (let p = 0;p < physical.length; p++) {
const content = physical[p];
const lineNo = p + 1;
const trimmed = content.trim();
if (trimmed === "")
continue;
if (trimmed.startsWith("#")) {
const indent2 = leadingIndent(content, lineNo);
pending.push({ indent: indent2, text: content.slice(content.indexOf("#") + 1) });
continue;
}
const indent = leadingIndent(content, lineNo);
const { tokens, trailing } = tokenizeLine(content, lineNo);
if (tokens.length === 0)
continue;
const leading = [];
for (const c of pending) {
if (c.indent > indent)
attachTail(c);
else
leading.push(c.text);
}
pending = [];
const node = { indent, line: lineNo, col: tokens[0].col, tokens, children: [] };
if (leading.length > 0)
node.leading = leading;
if (trailing !== undefined)
node.trailing = trailing;
while (stack.length > 0 && stack[stack.length - 1].indent >= indent)
stack.pop();
if (stack.length === 0)
roots.push(node);
else
stack[stack.length - 1].children.push(node);
stack.push(node);
}
for (const c of pending)
attachTail(c);
return { roots, tail: moduleTail };
}
function flatten(node) {
const out = [...node.tokens];
for (const child of node.children)
out.push(...flatten(child));
return out;
}
// src/lang/vocab.ts
function keysOf(table) {
return Object.keys(table);
}
var TIME_UNIT_TABLE = {
s: true,
m: true,
h: true,
d: true,
w: true,
mo: true,
q: true,
y: true
};
var TIME_UNITS_LIST = keysOf(TIME_UNIT_TABLE);
var TIME_UNITS = new Set(TIME_UNITS_LIST);
var ORDINAL_PREFIX = "d";
var EXPIRY_PREFIX = "e";
var ANCHOR_TABLE = {
fair: true,
intrinsic: true,
basis: true,
bid: true,
ask: true,
mid: true,
last: true,
join: true,
improve: true,
stub: true,
spot: true
};
var ANCHOR_NAMES = keysOf(ANCHOR_TABLE);
var ANCHORS = new Set(ANCHOR_NAMES);
var TIME_STOP_TABLE = {
"held-stop": "held",
"clock-stop": "clockET"
};
var HELD_STOP_KEYWORD = TIME_STOP_TABLE["held-stop"];
var CLOCK_STOP_KEYWORD = TIME_STOP_TABLE["clock-stop"];
var FILL_EVENT_TABLE = {
filled: true,
"partial-fill": true,
unfilled: true,
rejected: true,
cancelled: true
};
var FILL_EVENTS = new Set(keysOf(FILL_EVENT_TABLE));
var STANDING_TABLE = {
authored: true,
armed: true,
versioned: true,
superseded: true
};
var STANDINGS = new Set(keysOf(STANDING_TABLE));
var GRADE_WHAT_TABLE = {
plan: true,
wake: true,
view: true,
pod: true,
tag: true
};
var GRADE_WHATS = new Set(keysOf(GRADE_WHAT_TABLE));
var CITATION_ALGO_TABLE = {
sha256: true
};
var CITATION_ALGOS = new Set(keysOf(CITATION_ALGO_TABLE));
var PROV_RANK = {
unvetted: 0,
candidate: 1,
vetted: 2
};
var PROV_TIERS = new Set(keysOf(PROV_RANK));
var CMP_TO_PUNCT = {
gt: ">",
ge: ">=",
lt: "<",
le: "<=",
eq: "==",
ne: "!="
};
var CMP_FROM_PUNCT = Object.fromEntries(Object.entries(CMP_TO_PUNCT).map(([op, punct]) => [punct, op]));
var ORDINALS = [
"zeroth",
"first",
"second",
"third",
"fourth",
"fifth",
"sixth",
"seventh",
"eighth",
"ninth",
"tenth",
"eleventh",
"twelfth"
];
// src/lang/validate.ts
function assertNever(x, ctx) {
throw new Error(`kestrel/validate: unhandled variant in ${ctx}: ${JSON.stringify(x)}`);
}
var ATOMIC_MESSAGE = "the `atomic` keyword is reserved and refused in v1: an atomic multi-leg structure " + "needs a whole-structure preflight and an atomic execution adapter, which do not exist " + "yet (ADR-0005). Kestrel refuses rather than silently leg a named structure sequentially " + "(text that parses but lies). Author the legs as separate single-leg tickets, or wait for " + "atomic support";
var ATOMIC_RESERVED = "atomic is reserved: refused until whole-structure preflight + atomic execution adapter exist";
function refuseAtomic(atomic) {
if (atomic === true)
throw new Error(ATOMIC_RESERVED);
}
var HELD_CROSS_MESSAGE = "a cross cannot be `held`: a cross is an edge event, not a level, so sustaining it for a " + "duration is the anti-pattern that phantom-fired 10/12 in graded sim on a single anomalous " + "print (xrearm-1). To reject a lone spurious tick, give the cross a re-arm band " + "(`spot crosses above hod band 5c`); to require persistence, hold a level comparison instead " + "(`spot > hod held 30s`). `within` over a cross stays legal (the cross occurred inside a window)";
var HELD_CROSS_RESERVED = "a cross cannot be held: a cross is an edge event, not a level — use a re-arm band instead of holding the edge (xrearm-1)";
function refuseHeldOverCross(inner) {
if (inner.kind === "cross")
throw new Error(HELD_CROSS_RESERVED);
}
var BAND_POSITIVE_RESERVED = "a cross re-arm band must be a positive width: a zero/negative band can never re-arm — the cross would never clear its band to fire again (xrearm-1)";
function bandMessage(widthText) {
return `a cross re-arm band must be a positive width (got ${widthText}); a zero/negative ` + `band cannot re-arm — the cross would never clear its band to fire again`;
}
function refuseNonPositiveBand(band) {
if (band !== undefined && !(band.value > 0))
throw new Error(BAND_POSITIVE_RESERVED);
}
function budgetMessage(value) {
return `a risk budget must be a positive risk fraction (got ${value}R); \`size × max_loss ≤ budget\` requires budget > 0`;
}
function refuseNonPositiveBudget(value) {
if (!(value > 0))
throw new Error(budgetMessage(value));
}
var MARK_ANCHORS = new Set(["mid", "bid", "ask", "last", "mark", "premium"]);
function isSimpleSeries(op) {
return op.kind === "series" && op.segments.length === 1 && op.segments[0].selector === undefined && op.window === undefined;
}
function markOperand(op) {
return isSimpleSeries(op) && MARK_ANCHORS.has(op.segments[0].name) ? op.segments[0].name : undefined;
}
function findMarkInTrigger(t) {
switch (t.kind) {
case "cmp":
case "cross":
return markOperand(t.left) ?? markOperand(t.right);
case "event":
return t.of === undefined ? undefined : markOperand(t.of);
case "break-hold":
case "within":
case "until":
case "at":
return findMarkInTrigger(t.inner);
case "nth":
return findMarkInTrigger(t.event);
case "not":
return findMarkInTrigger(t.term);
case "and":
case "or":
for (const term of t.terms) {
const m = findMarkInTrigger(term);
if (m !== undefined)
return m;
}
return;
case "phase":
case "time-window":
case "fill":
case "held-stop":
case "clock-stop":
return;
default:
return assertNever(t, "findMarkInTrigger");
}
}
function markMessage(mark) {
return `EXIT may not condition on the mark \`${mark}\`: marks lie (the observed ${mark} is a ` + `health signal, never a value; ADR-0005). Condition the exit on \`fair\`, \`spot\`, a ` + `structural level, or an org fact instead`;
}
function refuseExitOnMark(when) {
const mark = findMarkInTrigger(when);
if (mark !== undefined)
throw new Error(markMessage(mark));
}
function provenanceMessage(planTier, moduleTier) {
return `provenance may only narrow downward (ARCHITECTURE §6): a \`${planTier}\` plan cannot ` + `sit under a \`${moduleTier}\` module — authority narrows, it never elevates`;
}
function elevatesProvenance(s, moduleRank) {
if (s.kind === "plan" && s.provenance?.tier !== undefined && PROV_RANK[s.provenance.tier] > moduleRank) {
return s.provenance.tier;
}
return;
}
function refuseProvenanceElevation(moduleTier, statements) {
if (moduleTier === undefined)
return;
const modRank = PROV_RANK[moduleTier];
for (const s of statements) {
const bad = elevatesProvenance(s, modRank);
if (bad !== undefined)
throw new Error(provenanceMessage(bad, moduleTier));
}
}
var CITATION_HASH_HEX = /^[0-9a-f]{64}$/;
function isCitationHash(hash) {
return CITATION_HASH_HEX.test(hash);
}
function citationBadHashMessage(got) {
return `a \`because\` citation needs a content hash of the fixed shape \`sha256:<64 hex>\` ` + `(got \`${got}\`): a Thesis digest is 64 lowercase hex chars`;
}
var CITATION_INLINE_BODY_MESSAGE = "a `because` citation carries a content hash only — no inline body: the Thesis prose lives " + "platform-side (referenced by `sha256:<64 hex>`), so drop the inline text after the digest";
function citationOnMessage(kind) {
return `a \`because\` citation may only pre-register a Plan or Wake, not a ${kind}: pre-registration ` + `binds an executable expectation to its thesis, which a ${kind} does not author`;
}
var CITATION_BAD_HASH_RESERVED = "a `because` citation needs a content hash of the fixed shape `sha256:<64 hex>`: a Thesis digest is 64 lowercase hex chars";
function refuseBadCitation(c) {
if (c.algo !== "sha256" || !isCitationHash(c.hash))
throw new Error(CITATION_BAD_HASH_RESERVED);
}
// src/lang/parse.ts
var CLAUSE_KEYWORDS = "WHEN, USING, DO, ALSO, RELOAD, TP, EXIT, INVALIDATE, CANCEL-IF, or ARM";
var PLAN_CLAUSE_KEYWORD_SET = new Set([
"WHEN",
"USING",
"DO",
"ALSO",
"RELOAD",
"TP",
"EXIT",
"INVALIDATE",
"CANCEL-IF",
"ARM"
]);
var TRIGGER_VERB_SYNONYMS = new Set([
"breaks",
"break",
"dips",
"dip",
"rises",
"rise",
"falls",
"fall",
"moves",
"move",
"drops",
"drop",
"climbs",
"climb",
"jumps",
"jump",
"hits",
"hit",
"reaches",
"reach",
"exceeds",
"exceed"
]);
function triggerVerbSynonymMessage(verb) {
return `unexpected \`${verb}\` in a trigger — Kestrel has no \`${verb}\` trigger verb. ` + `Write a threshold event as \`crosses above|below\` (a strict crossing) or ` + `\`touches above|below\` (at-or-touch), e.g. \`spot crosses below 499.15\` — ` + `or a plain comparison with \`<\`/\`>\`, e.g. \`spot < 499.15\`.`;
}
var PRICE_ANCHOR_SYNONYMS = new Set(["ceiling", "floor", "limit"]);
function priceAnchorSynonymMessage(tok) {
return `unexpected \`${tok}\` — Kestrel has no \`${tok}\` price anchor. ` + `To bound a price use \`min(...)\` to cap it or \`max(...)\` to floor it, ` + `or \`lean(a,b,x)\` to lean a fraction between two anchors — ` + `e.g. \`min(fair, 0.95)\` caps the price at 0.95.`;
}
function describe(t) {
if (t === undefined)
return "end of line";
if (t.type === "string")
return "a string";
if (t.type === "number")
return `number ${t.text}`;
return `\`${t.text}\``;
}
function lineCommentOf(node, at) {
const leading = node.leading;
const trailing = node.trailing;
const hasLeading = leading !== undefined && leading.length > 0;
if (!hasLeading && trailing === undefined)
return;
return {
at,
...hasLeading ? { leading } : {},
...trailing !== undefined ? { trailing } : {}
};
}
function commentLayer(header, interior) {
const lines = [];
const h = lineCommentOf(header, 0);
if (h !== undefined)
lines.push(h);
interior.forEach((n, i) => {
const lc = lineCommentOf(n, i + 1);
if (lc !== undefined)
lines.push(lc);
});
const tail = header.tail;
const hasTail = tail !== undefined && tail.length > 0;
if (lines.length === 0 && !hasTail)
return;
return {
...lines.length > 0 ? { lines } : {},
...hasTail ? { tail } : {}
};
}
function withComments(layer) {
return layer === undefined ? {} : { comments: layer };
}
class Cursor {
toks;
fallbackLine;
i = 0;
constructor(toks, fallbackLine) {
this.toks = toks;
this.fallbackLine = fallbackLine;
}
peek(k = 0) {
return this.toks[this.i + k];
}
wordAt(k = 0) {
const t = this.toks[this.i + k];
return t !== undefined && t.type === "word" ? t.text : undefined;
}
atEnd() {
return this.i >= this.toks.length;
}
here() {
const t = this.toks[this.i];
if (t !== undefined)
return { line: t.line, col: t.col };
const last = this.toks[this.toks.length - 1];
return last !== undefined ? { line: last.line, col: last.end } : { line: this.fallbackLine, col: 1 };
}
fail(message) {
const { line, col } = this.here();
throw new KestrelParseError(message, line, col);
}
advance() {
const t = this.toks[this.i];
if (t === undefined)
this.fail("unexpected end of line");
this.i++;
return t;
}
matchWord(text) {
return this.wordAt() === text;
}
optWord(text) {
if (this.matchWord(text)) {
this.i++;
return true;
}
return false;
}
expectWord(text, ctx) {
if (!this.matchWord(text))
this.fail(`expected \`${text}\` ${ctx}, got ${describe(this.peek())}`);
this.i++;
}
matchPunct(text) {
const t = this.peek();
return t !== undefined && t.type === "punct" && t.text === text;
}
optPunct(text) {
if (this.matchPunct(text)) {
this.i++;
return true;
}
return false;
}
expectPunct(text, ctx) {
if (!this.matchPunct(text))
this.fail(`expected \`${text}\` ${ctx}, got ${describe(this.peek())}`);
this.i++;
}
expectString(ctx) {
const t = this.peek();
if (t === undefined || t.type !== "string")
this.fail(`expected a quoted string ${ctx}, got ${describe(t)}`);
this.i++;
return t.text;
}
expectNumber(ctx) {
const t = this.peek();
if (t === undefined || t.type !== "number")
this.fail(`expected a number ${ctx}, got ${describe(t)}`);
this.i++;
return Number(t.text);
}
expectEnd(ctx) {
if (!this.atEnd())
this.fail(`unexpected ${describe(this.peek())} after ${ctx}`);
}
readName(what) {
const first = this.peek();
if (first === undefined || first.type !== "word")
this.fail(`expected ${what}, got ${describe(first)}`);
let text = first.text;
let end = first.end;
this.i++;
for (;; ) {
const t = this.peek();
if (t === undefined || t.col !== end)
break;
if (t.type === "punct" && (t.text === "-" || t.text === "_")) {
const after = this.peek(1);
if (after !== undefined && after.col === t.end && (after.type === "word" || after.type === "number")) {
text += t.text + after.text;
end = after.end;
this.i += 2;
continue;
}
break;
}
if (t.type === "word" || t.type === "number") {
text += t.text;
end = t.end;
this.i++;
continue;
}
break;
}
return text;
}
}
function expectClauseEnd(c, clauseCtx) {
const t = c.peek();
if (t !== undefined && t.type === "word" && PLAN_CLAUSE_KEYWORD_SET.has(t.text)) {
c.fail(`unexpected \`${t.text}\` after ${clauseCtx} — this looks like two clauses on one line. ` + `Each PLAN clause goes on its OWN line, indented two spaces beneath the \`PLAN <name>\` header: ` + `write \`WHEN …\` and \`DO …\` as SEPARATE lines, never \`WHEN … DO …\` together.`);
}
c.expectEnd(clauseCtx);
}
function readTimeUnit(c, ctx) {
const units = TIME_UNITS_LIST.join(", ");
const u = c.readName(`a time unit (${units}) ${ctx}`);
if (!TIME_UNITS.has(u))
c.fail(`unknown time unit \`${u}\`; expected one of ${units}`);
return u;
}
function readWindow(c) {
const value = c.expectNumber("for a window size");
const unit = readTimeUnit(c, "for a window");
return { kind: "window", value, unit };
}
function readDuration(c, ctx) {
const value = c.expectNumber(`for a duration ${ctx}`);
const unit = readTimeUnit(c, ctx);
return { kind: "duration", value, unit };
}
function readTimeOfDay(c) {
const hour = c.expectNumber("for the hour of a clock time");
c.expectPunct(":", "in a clock time (HH:MM)");
const minute = c.expectNumber("for the minute of a clock time");
if (!Number.isInteger(hour) || hour < 0 || hour > 23)
c.fail(`bad clock hour ${hour}; expected 0..23`);
if (!Number.isInteger(minute) || minute < 0 || minute > 59)
c.fail(`bad clock minute ${minute}; expected 0..59`);
return { kind: "time-of-day", hour, minute };
}
function readQuantity(c, ctx) {
let sign = 1;
if (c.matchPunct("+"))
c.advance();
else if (c.matchPunct("-")) {
c.advance();
sign = -1;
}
const value = sign * c.expectNumber(ctx);
const unit = readOptUnit(c);
return unit === undefined ? { kind: "quantity", value } : { kind: "quantity", value, unit };
}
function readBand(c) {
const q = readQuantity(c, "for a cross re-arm band width (`band 5c`)");
if (!(q.value > 0))
c.fail(bandMessage(quantityText(q)));
return q;
}
function quantityText(q) {
return q.unit === undefined ? String(q.value) : `${q.value}${q.unit}`;
}
function readOptUnit(c) {
if (c.matchPunct("%")) {
c.advance();
return "%";
}
const w = c.wordAt();
if (w === "R" || w === "c" || w === "bp") {
c.advance();
return w;
}
return;
}
function parseSeries(c) {
const segments = [];
for (;; ) {
const name = c.readName("a series name");
if (c.matchPunct("(") && c.peek(1)?.type === "word") {
c.advance();
const q = c.wordAt();
let selector;
if (q === "any" || q === "all") {
c.advance();
selector = { kind: "sel-quant", q };
} else {
selector = { kind: "sel-name", name: c.readName("a selector name inside `(...)`") };
}
c.expectPunct(")", "to close a series selector");
segments.push({ name, selector });
} else {
segments.push({ name });
}
if (c.optPunct("."))
continue;
break;
}
let window;
if (c.matchPunct("(") && c.peek(1)?.type === "number") {
c.advance();
window = readWindow(c);
c.expectPunct(")", "to close a series window");
}
return window === undefined ? { kind: "series", segments } : { kind: "series", segments, window };
}
function parseOperand(c) {
const t = c.peek();
if (t !== undefined && (t.type === "number" || t.type === "punct" && (t.text === "+" || t.text === "-"))) {
let sign = 1;
if (c.matchPunct("+"))
c.advance();
else if (c.matchPunct("-")) {
c.advance();
sign = -1;
}
const value = c.expectNumber("for an operand");
if (c.matchWord("sigma")) {
c.advance();
return { kind: "baseline", stat: "sigma", n: value };
}
const unit = readOptUnit(c);
return unit === undefined ? { kind: "quantity", value: sign * value } : { kind: "quantity", value: sign * value, unit };
}
const w = c.wordAt();
if (w !== undefined && /^p\d+$/.test(w)) {
c.advance();
return { kind: "baseline", stat: "p", n: Number(w.slice(1)) };
}
if (w !== undefined)
return parseSeries(c);
c.fail(`expected an operand (a series name, a number, or a baseline like p99/3sigma), got ${describe(t)}`);
}
function parseTrigger(c) {
return parseOr(c);
}
function parseOr(c) {
const terms = [parseAnd(c)];
while (c.optWord("OR"))
terms.push(parseAnd(c));
return terms.length === 1 ? terms[0] : { kind: "or", terms };
}
function parseAnd(c) {
const terms = [parseNot(c)];
while (c.optWord("AND"))
terms.push(parseNot(c));
return terms.length === 1 ? terms[0] : { kind: "and", terms };
}
function parseNot(c) {
if (c.optWord("NOT"))
return { kind: "not", term: parseNot(c) };
return parsePostfix(c);
}
function parsePostfix(c) {
let inner = parseAtom(c);
for (;; ) {
if (c.matchWord("held")) {
if (inner.kind === "cross")
c.fail(HELD_CROSS_MESSAGE);
c.advance();
inner = { kind: "break-hold", inner, dur: readDuration(c, "after `held`") };
} else if (c.matchWord("within")) {
c.advance();
inner = { kind: "within", inner, dur: readDuration(c, "after `within`") };
} else if (c.matchWord("until")) {
c.advance();
inner = { kind: "until", inner, at: readTimeOfDay(c) };
} else if (c.matchWord("at")) {
c.advance();
inner = { kind: "at", inner, at: readTimeOfDay(c) };
} else
break;
}
return inner;
}
function ordinalValue(c) {
const w = c.wordAt();
if (w !== undefined) {
const idx = ORDINALS.indexOf(w);
if (idx >= 1)
return idx;
}
const t = c.peek();
if (t !== undefined && t.type === "number" && c.wordAt(1) === "th")
return Number(t.text);
return;
}
function parseAtom(c) {
if (c.optPunct("(")) {
const t = parseTrigger(c);
c.expectPunct(")", "to close a parenthesized trigger");
return t;
}
const ord = ordinalValue(c);
if (ord !== undefined) {
if (c.wordAt() !== undefined)
c.advance();
else {
c.advance();
c.advance();
}
return { kind: "nth", ordinal: ord, event: parseAtom(c) };
}
if (c.matchWord("phase")) {
c.advance();
return { kind: "phase", phase: c.readName("a phase name after `phase`") };
}
if (c.matchWord("time")) {
c.advance();
return parseTimeWindow(c);
}
if (c.matchWord(HELD_STOP_KEYWORD) && c.peek(1)?.type === "number") {
c.advance();
return { kind: "held-stop", dur: readDuration(c, "after `held` (a 0DTE time-held stop, `held 90m`)") };
}
if (c.matchWord(CLOCK_STOP_KEYWORD)) {
c.advance();
return { kind: "clock-stop", at: readTimeOfDay(c) };
}
const w = c.wordAt();
if (w !== undefined && FILL_EVENTS.has(w)) {
c.advance();
const event = w;
if (c.optWord("leg")) {
const leg = c.expectNumber("for a fill-event leg index");
return { kind: "fill", event, leg };
}
return { kind: "fill", event };
}
return parseOperandLed(c);
}
function parseTimeWindow(c) {
if (c.optWord("after"))
return { kind: "time-window", from: readTimeOfDay(c) };
if (c.optWord("before"))
return { kind: "time-window", to: readTimeOfDay(c) };
const from = readTimeOfDay(c);
c.expectPunct("..", "in a time window (`time HH:MM..HH:MM`)");
const to = readTimeOfDay(c);
return { kind: "time-window", from, to };
}
function parseOperandLed(c) {
const left = parseOperand(c);
if (c.matchWord("crosses") || c.matchWord("touches")) {
const touch = c.advance().text === "touches";
const kw = touch ? "touches" : "crosses";
if (c.optWord("outside")) {
const lo = parseOperand(c);
c.expectPunct("-", "between the low and high of a breakout range (`crosses outside LO-HI`)");
const hi = parseOperand(c);
const touchOpt = touch ? { touch: true } : {};
const above = { kind: "cross", left, dir: "above", right: hi, ...touchOpt };
const below = { kind: "cross", left, dir: "below", right: lo, ...touchOpt };
return { kind: "or", terms: [above, below] };
}
let dir;
if (c.optWord("above"))
dir = "above";
else if (c.optWord("below"))
dir = "below";
else
c.fail(`expected \`above\`, \`below\`, or \`outside LO-HI\` (a breakout) after \`${kw}\`, got ${describe(c.peek())}`);
const right = parseOperand(c);
let band;
if (c.optWord("band"))
band = readBand(c);
return {
kind: "cross",
left,
dir,
right,
...touch ? { touch } : {},
...band !== undefined ? { band } : {}
};
}
const opTok = c.peek();
if (opTok !== undefined && opTok.type === "punct" && opTok.text in CMP_FROM_PUNCT) {
c.advance();
const op = CMP_FROM_PUNCT[opTok.text];
return { kind: "cmp", op, left, right: parseOperand(c) };
}
const infix = c.wordAt();
if (infix !== undefined && TRIGGER_VERB_SYNONYMS.has(infix))
c.fail(triggerVerbSynonymMessage(infix));
if (isSimpleSeries(left)) {
const name = left.segments[0].name;
if (c.optWord("of"))
return { kind: "event", name, of: parseOperand(c) };
return { kind: "event", name };
}
c.fail(`expected a comparison (>, >=, <, <=, ==, !=), \`crosses above/below\`, or \`of\` ` + `after this operand, got ${describe(c.peek())}`);
}
function parsePriceAtom(c) {
const t = c.peek();
if (t !== undefined && t.type === "word") {
if (ANCHORS.has(t.text)) {
c.advance();
return { kind: "anchor", name: t.text };
}
if (t.text === "min" || t.text === "max") {
c.advance();
c.expectPunct("(", `after \`${t.text}\``);
const args = [parsePrice(c)];
while (c.optPunct(","))
args.push(parsePrice(c));
c.expectPunct(")", `to close \`${t.text}(...)\``);
return { kind: "price-fn", fn: t.text, args };
}
if (t.text === "cap") {
c.advance();
const args = [parsePrice(c)];
while (c.optPunct(","))
args.push(parsePrice(c));
if (args.length < 2) {
c.fail("`cap` needs a ceiling — write `cap <price>, <ceiling>` (it normalizes to " + "`min(<price>, <ceiling>)`), e.g. `cap fair, 0.95`");
}
return { kind: "price-fn", fn: "min", args };
}
if (t.text === "lean") {
c.advance();
c.expectPunct("(", "after `lean`");
const a = parsePrice(c);
c.expectPunct(",", "between lean arguments");
const b = parsePrice(c);
c.expectPunct(",", "between lean arguments");
const x = c.expectNumber("for the lean fraction");
c.expectPunct(")", "to close `lean(...)`");
return { kind: "lean", a, b, x };
}
}
if (t !== undefined && t.type === "number") {
c.advance();
return { kind: "price-abs", value: Number(t.text) };
}
if (t !== undefined && t.type === "word" && PRICE_ANCHOR_SYNONYMS.has(t.text)) {
c.fail(priceAnchorSynonymMessage(t.text));
}
c.fail(`expected a price: an anchor (${ANCHOR_NAMES.join(", ")}), a number, or min/max/lean(...), ` + `got ${describe(t)}`);
}
function parsePrice(c) {
let base = parsePriceAtom(c);
for (;; ) {
const t = c.peek();
const next = c.peek(1);
if (t !== undefined && t.type === "punct" && (t.text === "+" || t.text === "-") && next?.type === "number") {
c.advance();
const amount = c.expectNumber("for a price offset");
let unit;
if (c.optPunct("%"))
unit = "%";
else if (c.matchWord("c")) {
c.advance();
unit = "c";
} else {
c.fail(`expected \`c\` or \`%\` after a price offset amount, got ${describe(c.peek())}`);
}
base = { kind: "price-offset", base, sign: t.text, amount, unit };
continue;
}
break;
}
return base;
}
function parseStrike(c) {
if (c.optWord("atm"))
return { kind: "strike-atm" };
if (c.matchPunct("+") || c.matchPunct("-")) {
const sign = c.advance().text === "-" ? -1 : 1;
return { kind: "strike-rel", steps: sign * c.expectNumber("for a relative strike") };
}
const t = c.peek();
if (t !== undefined && t.type === "number") {
c.advance();
if (c.matchWord("d")) {
c.advance();
return { kind: "strike-delta", delta: Number(t.text) };
}
return { kind: "strike-abs", strike: Number(t.text) };
}
c.fail(`expected a strike (+N/-N relative, N absolute, atm, or Nd delta) for an option leg, ` + `or \`shares\` for an equity leg (\`buy N shares\`, ADR-0017), got ${describe(t)}`);
}
function parseLeg(c) {
let side;
if (c.optWord("buy"))
side = "buy";
else if (c.optWord("sell"))
side = "sell";
else
c.fail(`expected \`buy\` or \`sell\` to start a leg, got ${describe(c.peek())}`);
const qty = c.expectNumber("for a leg quantity");
if (c.optWord("shares")) {
const t = c.peek();
const w = c.wordAt();
if (t !== undefined && t.type === "number" || w === "C" || w === "P" || w === "atm" || c.matchPunct("+") || c.matchPunct("-")) {
c.fail(`an equity/spot leg carries no strike or right — \`${qty} shares\` is complete (ADR-0017); drop the strike/right`);
}
return { kind: "equity-leg", side, qty };
}
const strike = parseStrike(c);
let right;
if (c.optWord("C"))
right = "C";
else if (c.optWord("P"))
right = "P";
else
c.fail(`expected \`C\` or \`P\` (option right) after a strike, got ${describe(c.peek())}`);
let expiry;
if (c.optWord("exp")) {
const t = c.peek();
if (t === undefined || t.type === "punct") {
c.fail(`\`exp\` needs an expiry selector after it — \`0dte\`, a date (\`2026-07-17\`), or a tag (\`weekly\`) — got ${describe(t)}`);
}
expiry = parseExpiry(c);
if (expiry === undefined)
c.fail("`exp` needs an expiry selector after it (`0dte`, a date like `2026-07-17`, or a tag like `weekly`)");
}
return { kind: "leg", side, qty, strike, right, ...expiry !== undefined ? { expiry } : {} };
}
function parseLegs(c) {
const legs = [parseLeg(c)];
while (c.optPunct(","))
legs.push(parseLeg(c));
return legs;
}
function parseHeldQuantifier(c) {
if (c.matchWord("foreach")) {
c.advance();
c.expectWord("held", "in a held-leg quantifier (`foreach held leg`)");
c.expectWord("leg", "in a held-leg quantifier (`foreach held leg`)");
return { kind: "held-foreach" };
}
if (c.matchWord("any")) {
c.advance();
c.expectWord("held", "in a held-leg quantifier (`any held leg`)");
c.expectWord("leg", "in a held-leg quantifier (`any held leg`)");
return { kind: "held-any" };
}
if (c.wordAt() === "held" && c.wordAt(1) === "leg") {
c.fail("a held-leg quantifier needs a `foreach` or `any` head — `held leg` alone is not a quantifier. " + "Did you mean `foreach held leg` (every held leg) or `any held leg`?");
}
return;
}
function parseOrderPolicy(c) {
let pricing;
const esc = [];
const caps = [];
const floors = [];
let cancelIf;
let gtc = false;
for (;; ) {
if (c.matchWord("peg") || c.matchWord("fix")) {
if (pricing !== undefined)
c.fail("a ticket may declare `peg` or `fix` only once");
pricing = c.advance().text;
} else if (c.optWord("esc")) {
const to = parsePrice(c);
esc.push({ kind: "esc-stage", to, after: readDuration(c, "for an esc stage") });
} else if (c.optWord("cap")) {
caps.push(parsePrice(c));
} else if (c.optWord("floor")) {
floors.push(parsePrice(c));
} else if (c.optWord("cancel-if")) {
cancelIf = parseTrigger(c);
} else if (c.optWord("gtc")) {
gtc = true;
} else
break;
}
if (pricing === undefined && esc.length === 0 && caps.length === 0 && floors.length === 0 && cancelIf === undefined && !gtc) {
return;
}
return {
kind: "order-policy",
...pricing !== undefined ? { pricing } : {},
...esc.length > 0 ? { esc } : {},
...caps.length > 0 ? { caps } : {},
...floors.length > 0 ? { floors } : {},
...cancelIf !== undefined ? { cancelIf } : {},
...gtc ? { gtc } : {}
};
}
function rejectAtomicIfPresent(c) {
if (c.matchWord("atomic"))
c.fail(ATOMIC_MESSAGE);
}
function parseTpTarget(c) {
if (c.matchPunct("+") || c.matchPunct("-")) {
const sign = c.advance().text === "-" ? -1 : 1;
const pct = sign * c.expectNumber("for a TP percentage");
c.expectPunct("%", "after a TP percentage (`+100%`)");
return { kind: "tp-pct", pct };
}
const t = c.peek();
if (t !== undefined && t.type === "number") {
if (c.peek(1)?.type === "punct" && c.peek(1).text === "%") {
c.advance();
c.advance();
return { kind: "tp-pct", pct: Number(t.text) };
}
if (c.wordAt(1) === "x") {
c.advance();
c.advance();
return { kind: "tp-mult", mult: Number(t.text) };
}
}
return { kind: "tp-price", price: parsePrice(c) };
}
function parsePlanClause(c) {
const kw = c.readName(`a clause keyword (${CLAUSE_KEYWORDS})`);
switch (kw) {
case "DO":
case "ALSO": {
rejectAtomicIfPresent(c);
const legs = parseLegs(c);
c.expectPunct("@", "before the ticket price");
const price = parsePrice(c);
const policy = parseOrderPolicy(c);
expectClauseEnd(c, `the ${kw} ticket`);
const base = { legs, price, ...policy !== undefined ? { policy } : {} };
return kw === "DO" ? { kind: "do", ...base } : { kind: "also", ...base };
}
case "RELOAD": {
let when;
if (c.optWord("WHEN"))
when = parseTrigger(c);
rejectAtomicIfPresent(c);
const legs = parseLegs(c);
c.expectPunct("@", "before the RELOAD price");
const price = parsePrice(c);
const policy = parseOrderPolicy(c);
expectClauseEnd(c, "the RELOAD ticket");
return {
kind: "reload",
...when !== undefined ? { when } : {},
legs,
price,
...policy !== undefined ? { policy } : {}
};
}
case "TP": {
const target = parseTpTarget(c);
let frac;
if (c.optWord("frac"))
frac = c.expectNumber("for a TP fraction");
const over = parseHeldQuantifier(c);
let price;
if (c.optPunct("@"))
price = parsePrice(c);
const policy = parseOrderPolicy(c);
expectClauseEnd(c, "the TP clause");
return {
kind: "tp",
target,
...frac !== undefined ? { frac } : {},
...over !== undefined ? { over } : {},
...price !== undefined ? { price } : {},
...policy !== undefined ? { policy } : {}
};
}
case "EXIT": {
const when = parseTrigger(c);
const mark = findMarkInTrigger(when);
if (mark !== undefined)
c.fail(markMessage(mark));
const over = parseHeldQuantifier(c);
let price;
if (c.optPunct("@"))
price = parsePrice(c);
const policy = parseOrderPolicy(c);
expectClauseEnd(c, "the EXIT clause");
return {
kind: "exit",
when,
...over !== undefined ? { over } : {},
...price !== undefined ? { price } : {},
...policy !== undefined ? { policy } : {}
};
}
case "INVALIDATE": {
const when = parseTrigger(c);
expectClauseEnd(c, "the INVALIDATE clause");
return { kind: "invalidate", when };
}
case "CANCEL-IF": {
const when = parseTrigger(c);
expectClauseEnd(c, "the CANCEL-IF clause");
return { kind: "cancel-if", when };
}
case "ARM": {
let when;
if (c.optWord("WHEN"))
when = parseTrigger(c);
let basis;
if (c.optWord("basis"))
basis = parsePrice(c);
const over = parseHeldQuantifier(c);
if (when === undefined && over === undefined) {
c.fail(basis === undefined ? "bare `ARM` binds nothing — an ARM clause must adopt a held leg or chain a plan. " + "Did you mean `ARM foreach held leg` (adopt a superseded plan's held leg — the exit " + "escape hatch) or `ARM WHEN <trigger>` (chain plans on another's state)?" : "`ARM basis <px>` carries an adoption basis but binds no leg — a basis only anchors an " + "ADOPTED held leg (RUNTIME §4). Did you mean `ARM basis <px> foreach held leg` (adopt " + "the superseded plan's held leg at that basis)?");
}
expectClauseEnd(c, "the ARM clause");
return {
kind: "arm",
...when !== undefined ? { when } : {},
...basis !== undefined ? { basis } : {},
...over !== undefined ? { over } : {}
};
}
default:
c.fail(`unknown clause \`${kw}\`; expected one of ${CLAUSE_KEYWORDS}`);
}
}
function parseExpiry(c) {
const t = c.peek();
if (t === undefined)
return;
if (t.type === "number") {
c.advance();
if (c.matchWord("dte")) {
c.advance();
return { kind: "expiry-dte", dte: Number(t.text) };
}
if (c.matchPunct("-")) {
let s = t.text;
while (c.optPunct("-")) {
const seg = c.peek();
if (seg === undefined || seg.type !== "number")
c.fail(`expected a number after \`-\` in an expiry date, got ${describe(seg)}`);
s += "-" + seg.text;
c.advance();
}
return { kind: "expiry-date", date: s };
}
c.fail(`expected \`dte\` or a date after ${t.text} in an expiry, got ${describe(c.peek())}`);
}
return { kind: "expiry-tag", tag: c.readName("an expiry tag") };
}
function parseInstrument(c, opts) {
const symbol = c.readName("an instrument symbol");
if (!opts.expiry)
return { kind: "instrument", symbol };
const w = c.wordAt();
const nextIsStop = w !== undefined && opts.stop.has(w);
const hasExpiry = !c.atEnd() && !nextIsStop;
if (!hasExpiry)
return { kind: "instrument", symbol };
const expiry = parseExpiry(c);
return expiry === undefined ? { kind: "instrument", symbol } : { kind: "instrument", symbol, expiry };
}
var USING_STOP = new Set(["signal", "exec"]);
function parseUsingBody(c) {
let signal;
let exec;
for (;; ) {
if (c.optWord("signal"))
signal = parseInstrument(c, { expiry: true, stop: USING_STOP });
else if (c.optWord("exec"))
exec = parseInstrument(c, { expiry: true, stop: USING_STOP });
else
break;
}
if (signal === undefined && exec === undefined) {
c.fail("`USING` needs at least a `signal` or an `exec` instrument");
}
return {
kind: "using",
...signal !== undefined ? { signal } : {},
...exec !== undefined ? { exec } : {}
};
}
function parseProvenanceBrace(c) {
c.expectPunct("{", "to open a provenance map");
let tier;
let author;
let origin;
let replay;
if (!c.matchPunct("}")) {
for (;; ) {
const key = c.readName("a provenance field (tier, author, origin, or replay)");
c.expectPunct(":", "after a provenance field name");
switch (key) {
case "tier": {
const v = c.readName("a provenance tier (vetted, candidate, or unvetted)");
if (!PROV_TIERS.has(v))
c.fail(`unknown provenance tier \`${v}\`; expected vetted, candidate, or unvetted`);
tier = v;
break;
}
case "author":
author = c.expectString("for a provenance author");
break;
case "origin":
origin = c.expectString("for a provenance origin");
break;
case "replay":
replay = c.expectString("for a provenance replay record");
break;
default:
c.fail(`unknown provenance field \`${key}\`; expected tier, author, origin, or replay`);
}
if (!c.optPunct(","))
break;
}
}
c.expectPunct("}", "to close a provenance map");
return {
kind: "provenance",
...tier !== undefined ? { tier } : {},
...author !== undefined ? { author } : {},
...origin !== undefined ? { origin } : {},
...replay !== undefined ? { replay } : {}
};
}
function readContentHash(c) {
const first = c.peek();
if (first === undefined || first.type !== "word" && first.type !== "number") {
c.fail(citationBadHashMessage(describe(first)));
}
let text = first.text;
let end = first.end;
c.advance();
for (;; ) {
const t = c.peek();
if (t === undefined || t.col !== end)
break;
if (t.type !== "word" && t.type !== "number")
break;
text += t.text;
end = t.end;
c.advance();
}
return text;
}
function parseBecause(c) {
const algo = c.readName("a content-hash algorithm in a `because` citation (`sha256:<64 hex>`)");
if (!CITATION_ALGOS.has(algo))
c.fail(citationBadHashMessage(`${algo}:…`));
c.expectPunct(":", "between `sha256` and its digest in a `because` citation (`sha256:<64 hex>`)");
const hash = readContentHash(c);
if (!isCitationHash(hash))
c.fail(citationBadHashMessage(hash));
if (!c.atEnd())
c.fail(CITATION_INLINE_BODY_MESSAGE);
return { kind: "citation", algo, hash };
}
function mergeUsing(ambient, explicit) {
if (ambient === undefined && explicit === undefined)
return;
const signal = explicit?.signal ?? ambient?.signal;
const exec = explicit?.exec ?? ambient?.exec;
return {
kind: "using",
...signal !== undefined ? { signal } : {},
...exec !== undefined ? { exec } : {}
};
}
function headerCursor(node, keyword) {
const c = new Cursor(node.tokens, node.line);
c.expectWord(keyword, "to open the statement");
return c;
}
function parseView(node) {
const c = headerCursor(node, "VIEW");
const name = c.readName("a view name");
let budget;
if (c.optWord("budget"))
budget = c.expectNumber("for a view token budget");
c.expectEnd("the VIEW header");
for (const child of node.children) {
if (child.tokens[0]?.type === "word" && child.tokens[0].text === "BECAUSE") {
throw new KestrelParseError(citationOnMessage("View"), child.line, child.col);
}
}
const panes = node.children.map(parsePane);
return {
kind: "view",
name,
panes,
...budget !== undefined ? { budget } : {},
...withComments(commentLayer(node, node.children))
};
}
function parsePane(node) {
const c = new Cursor(flatten(node), node.line);
const name = c.readName("a pane name");
const args = [];
while (!c.atEnd()) {
const t = c.peek();
if (t.type === "punct" && (t.text === ":" || t.text === "{" || t.text === ",")) {
c.fail(`a pane is a NAME followed by plain space-separated arguments (e.g. \`chain fair realness\`, ` + `\`tape skyline 5m vwap\`) — not \`${t.text}\`. Drop any \`:\`/\`{…}\`/\`,\` and write one pane per ` + `indented line beneath the \`VIEW <name>\` header.`);
}
if (t.type === "word" && (t.text === ORDINAL_PREFIX || t.text === EXPIRY_PREFIX)) {
const dash = c.peek(1);
const numTok = c.peek(2);
if (dash !== undefined && dash.type === "punct" && dash.text === "-" && dash.col === t.end && numTok !== undefined && numTok.type === "number" && numTok.col === dash.end) {
c.advance();
c.advance();
const value = c.expectNumber(`for a ${t.text === ORDINAL_PREFIX ? "session ordinal (`d-<n>`)" : "expiry ordinal (`e-<n>`)"}`);
if (t.text === ORDINAL_PREFIX)
args.push({ kind: "arg-ordinal", ordinal: value });
else
args.push({ kind: "arg-expiry", expiry: value });
continue;
}
}
if (t.type === "number") {
const value = c.expectNumber("for a pane numeral argument");
const next = c.peek();
if (next !== undefined && next.type === "word" && TIME_UNITS.has(next.text)) {
const unit = readTimeUnit(c, "for a pane window argument");
args.push({ kind: "arg-window", window: { kind: "window", value, unit } });
} else {
args.push({ kind: "arg-count", count: value });
}
} else
args.push({ kind: "arg-ident", name: c.readName("a pane argument") });
}
return { kind: "pane", name, args };
}
function parseWake(node) {
const c = headerCursor(node, "WAKE");
const name = c.readName("a wake name");
c.expectEnd("the WAKE header");
let when;
let because;
let deliver;
let priority;
let coalesce;
let budget;
let universe;
for (const child of node.children) {
const cc = new Cursor(flatten(child), child.line);
const kw = cc.readName("a WAKE clause (WHEN, BECAUSE, DELIVER, PRIORITY, COALESCE, BUDGET, or UNIVERSE)");
switch (kw) {
case "WHEN":
when = parseTrigger(cc);
break;
case "BECAUSE":
if (because !== undefined)
cc.fail("a WAKE carries at most one `because` citation");
because = parseBecause(cc);
break;
case "DELIVER": {
const view = cc.readName("a view name after DELIVER");
const mandatory = cc.optWord("MANDATORY");
const keyframe = cc.optWord("KEYFRAME");
deliver = { kind: "deliver", view, ...mandatory ? { mandatory } : {}, ...keyframe ? { keyframe } : {} };
break;
}
case "PRIORITY":
priority = cc.expectNumber("for a wake priority");
break;
case "COALESCE":
coalesce = cc.readName("a coalesce key");
break;
case "BUDGET":
budget = parseWakeBudget(cc);
break;
case "UNIVERSE":
universe = { kind: "universe", universe: cc.readName("a universe name") };
break;
default:
cc.fail(`unknown WAKE clause \`${kw}\`; expected WHEN, BECAUSE, DELIVER, PRIORITY, COALESCE, BUDGET, or UNIVERSE`);
}
cc.expectEnd(`the ${kw} clause`);
}
if (when === undefined)
throw new KestrelParseError("a WAKE requires a WHEN clause", node.line, node.col);
return {
kind: "wake",
name,
when,
...because !== undefined ? { because } : {},
...deliver !== undefined ? { deliver } : {},
...priority !== undefined ? { priority } : {},
...coalesce !== undefined ? { coalesce } : {},
...budget !== undefined ? { budget } : {},
...universe !== undefined ? { universe } : {},
...withComments(commentLaye