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.
486 lines (443 loc) • 22.6 kB
text/typescript
/**
* # agent language card — a compiled, token-budgeted projection of the capability catalog (kestrel-gsh.4)
*
* The card (`docs/public/card.md` + `docs/public/card.json`) is the *compact* surface an agent author
* loads into context before writing Kestrel. It is **not** a hand-maintained prompt: a hand-written
* card would drift from what the runtime actually does, and a public claim a reader cannot verify is a
* lie the repo tells (AGENTS.md). So the card is a **pure projection of the typed capability catalog**
* (`tests/support/catalog.ts`) — the same four-axis, receipt-backed source of truth the status page
* (gsh.2) compiles from. Every capability, surface, and fail-closed doctrine on the card is therefore
* exactly what the catalog asserts, no more; a new capability changes the projection and a CI test
* (`tests/docs.card.test.ts`) fails until the committed card is regenerated.
*
* Three properties make the card trustworthy rather than merely pretty:
*
* 1. **Byte-determinism.** `renderCardMarkdown` / `renderCardJson` are pure functions of the catalog
* (+ the package version) with stable ordering and no wall clock: same catalog ⇒ byte-identical
* card. The CI test re-renders and compares byte-for-byte.
* 2. **Version coupling via a catalog hash.** {@link catalogHash} is a content hash of the catalog
* snapshot the card compiled from. A drifted catalog changes the hash, which changes the card,
* which fails the equality test — the card can never silently describe a stale catalog.
* 3. **A tested token budget.** The card is for agents under token constraints, so its own size is a
* first-class property: {@link countTokens} + {@link TOKEN_BUDGET} bound the Markdown card, and a
* test asserts it fits. Compactness never drops the fail-closed / never-naked / marks-lie /
* evidence-status semantics — those are load-bearing, not decoration.
*
* The card also carries a handful of **object ⇄ text** pairs (ADR-0004): the typed AST object IS the
* language and text is a byte-stable projection of it. Each pair's canonical text is *derived* by
* running the real `print(parse(text))`, and the card's test asserts the round-trip is a fixed point —
* the examples are executable receipts, not prose.
*
* This module is pure data + pure rendering over `tests/support/catalog.ts` and `src/lang`; it is NOT
* on the runtime path and mutates nothing.
*/
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import type { AccessSurface, Capability, EvidenceTier, RuntimeStatus, SyntaxStatus } from "./catalog.ts";
import { CAPABILITIES, CERTIFIED_COHORT, EVIDENCE_SOURCE } from "./catalog.ts";
import { REPO_ROOT } from "./fences.ts";
import { parse, print } from "../lang/index.ts";
// ── Declared, tested economy ────────────────────────────────────────────────────
/** The command the generator writes into the card's "regenerate with" banner. */
export const REGEN_COMMAND = "bun scripts/gen-card.ts";
/**
* The card's tokenizer, declared so its budget is reproducible. It is deliberately simple and
* self-contained (no model-specific vocabulary, no network): one token per contiguous run of
* word characters (`[A-Za-z0-9_]`) and one token per other non-whitespace character (each brace,
* pipe, backtick, colon, …). It over-counts relative to a BPE tokenizer, so a card that fits this
* budget fits a real model's budget with margin — the honest direction to err for a token guarantee.
*/
export const TOKENIZER = "kestrel-card/word-punct-v1";
/**
* The maximum token budget for the Markdown card under {@link TOKENIZER}. The card is what an agent
* pays for on every author turn, so this is a hard, tested ceiling — {@link countTokens} of the
* generated `card.md` must not exceed it (see `tests/docs.card.test.ts`). Chosen with headroom over
* the current card so incremental catalog growth does not trip CI, while still forcing a deliberate
* decision (raise the budget, or trim the card) if the card ever bloats.
*/
export const TOKEN_BUDGET = 6000;
/** Count tokens under {@link TOKENIZER}: word-runs + individual punctuation. Pure, deterministic. */
export function countTokens(text: string): number {
const m = text.match(/[A-Za-z0-9_]+|[^\sA-Za-z0-9_]/g);
return m === null ? 0 : m.length;
}
// ── Version coupling: a content hash of the catalog snapshot ─────────────────────
/**
* A canonical, order-independent serialization of one capability — the exact facts the card
* projects. Reordering the catalog array, or reordering a capability's receipts/examples/surfaces,
* must NOT change the hash; changing any asserted fact MUST. So every list is sorted and every field
* is spelled out.
*/
function canonicalCapability(c: Capability): string {
const surfaces = (Object.entries(c.access.surfaces) as [AccessSurface, string][])
.map(([s, l]) => `${s}:${l}`)
.sort();
return [
`id=${c.id}`,
`title=${c.title}`,
`summary=${c.summary}`,
`public=${c.public}`,
`syntax=${c.syntax.status}${c.syntax.note ? `|${c.syntax.note}` : ""}`,
`runtime=${c.runtime.status}${c.runtime.note ? `|${c.runtime.note}` : ""}`,
`evidence=${c.evidence.tier}${c.evidence.note ? `|${c.evidence.note}` : ""}`,
`access=${surfaces.join(",")}${c.access.note ? `|${c.access.note}` : ""}`,
`source=${[...c.receipts.source].sort().join(",")}`,
`test=${[...c.receipts.test].sort().join(",")}`,
`examples=${[...c.examples].sort().join(",")}`,
].join("\n");
}
/**
* The catalog hash the card stamps for version coupling: a SHA-256 over the canonical serialization
* of every capability (sorted by id) plus the certified-cohort invariant. Deterministic — no wall
* clock, no insertion-order dependence. A drifted catalog ⇒ a different hash ⇒ a stale card ⇒ CI red.
*/
export function catalogHash(capabilities: readonly Capability[] = CAPABILITIES): string {
const sorted = [...capabilities].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
const payload = [
`certified_cohort=${CERTIFIED_COHORT}`,
`evidence_source=${EVIDENCE_SOURCE}`,
...sorted.map(canonicalCapability),
].join("\n---\n");
return createHash("sha256").update(payload, "utf8").digest("hex");
}
/** Read the language version from `package.json` (deterministic; version-matched, no wall clock). */
export function readLanguageVersion(root: string = REPO_ROOT): string {
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { version?: string };
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
}
// ── Object ⇄ text examples (ADR-0004 round-trip, derived from the real parser/printer) ──
/** One object⇄text pair for the card. `text` is authored canonical; the round-trip is tested. */
export interface ObjectTextExample {
readonly title: string;
readonly note: string;
/** Authored canonical Kestrel text; the card derives its printed form via `print(parse(text))`. */
readonly text: string;
/** Show the full parsed AST object on the Markdown card (small statements only, to respect budget). */
readonly showObject: boolean;
}
/**
* The object⇄text pairs the card demonstrates. Each `text` is already byte-canonical, so
* `print(parse(text)) === text` (the card's test proves it). Generic tickers only (SPX/SPY) —
* never anything that reveals a strategy (ARCHITECTURE §7).
*/
export const OBJECT_TEXT_EXAMPLES: readonly ObjectTextExample[] = [
{
title: "Wake — a standing subscription over the trigger algebra",
note: "A Wake spends attention, never risk. Its `when` is the shared trigger algebra as a typed node.",
text: "WAKE momentum\n WHEN spot crosses above hod",
showObject: true,
},
{
title: "Grade — an imperative replay with counterfactuals",
note: "`VS` counterfactuals and `BY` dimensions are syntax, not config; the object mirrors the text one-for-one.",
text: "GRADE plan fade OVER 2024-01-01..2024-03-31 FILL maker-fair-v1\n VS ungated null\n BY vehicle",
showObject: true,
},
{
title: "Plan — bounded-risk contingent program (trigger → action → take-profit)",
note: "The printer elides USING-covered qualifiers and normalizes spacing; the object carries the full structure (see card.json).",
text:
"PLAN fade budget 0.3R ttl +30m\n" +
" WHEN spot crosses below vwap\n" +
" DO buy 1 -1 P @ lean(bid, fair, 0.5) peg\n" +
" TP 2x frac 0.5 @ fair",
showObject: false,
},
{
title: "Plan — an equity ticket (shares, not option strikes)",
note:
"An equity/spot leg is `buy N shares` — the `shares` marker replaces the strike+right an option leg carries " +
"(ADR-0017); the ambient `exec` symbol is the instrument. Omit `shares` and the parser refuses `buy N @ …`, " +
"naming both continuations.",
text:
"PLAN accumulate budget 0.5R ttl +30m\n" +
" WHEN spot crosses above vwap\n" +
" DO buy 100 shares @ spot",
showObject: false,
},
];
/** Derive the round-trip triple for one example: the parsed object + its canonical printed text. */
export function roundTrip(ex: ObjectTextExample): { object: unknown; printed: string } {
const object = parse(ex.text);
return { object, printed: print(object) };
}
// ── The machine card model (what card.json serializes) ──────────────────────────
const SURFACE_ORDER: readonly AccessSurface[] = ["text", "sdk", "cli", "mcp", "adapter"];
const SYNTAX_LABEL: Record<SyntaxStatus, string> = {
supported: "supported",
proposed: "proposed",
rejected: "rejected (fail-closed)",
};
const RUNTIME_LABEL: Record<RuntimeStatus, string> = {
implemented: "implemented",
partial: "partial",
none: "none",
};
const EVIDENCE_LABEL: Record<EvidenceTier, string> = {
none: "none",
mechanical: "mechanical (practice)",
certified: "certified",
};
/** The distinct access surfaces the catalog exposes, in canonical order (drives coverage). */
export function exposedSurfaces(capabilities: readonly Capability[] = CAPABILITIES): AccessSurface[] {
const seen = new Set<AccessSurface>();
for (const c of capabilities) {
if (!c.public) continue;
for (const s of Object.keys(c.access.surfaces) as AccessSurface[]) seen.add(s);
}
return SURFACE_ORDER.filter((s) => seen.has(s));
}
function accessString(c: Capability): string {
const parts: string[] = [];
for (const s of SURFACE_ORDER) {
const level = c.access.surfaces[s];
if (level !== undefined) parts.push(`${s}: ${level}`);
}
return parts.length === 0 ? "—" : parts.join(", ");
}
/** The sorted, public capabilities the card projects (private capabilities are omitted by design). */
export function cardCapabilities(capabilities: readonly Capability[] = CAPABILITIES): Capability[] {
return [...capabilities]
.filter((c) => c.public)
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
}
/** The fail-closed rejects, extracted from the catalog (syntax=rejected) — never dropped for brevity. */
export function rejects(capabilities: readonly Capability[] = CAPABILITIES): Capability[] {
return cardCapabilities(capabilities).filter((c) => c.syntax.status === "rejected");
}
// ── Deterministic JSON (stable key order: `kind` first, then alphabetical) ────────
function stableStringify(value: unknown, indent = 2): string {
const pad = (n: number): string => " ".repeat(n * indent);
const walk = (v: unknown, depth: number): string => {
if (v === null || typeof v !== "object") return JSON.stringify(v);
if (Array.isArray(v)) {
if (v.length === 0) return "[]";
const items = v.map((x) => pad(depth + 1) + walk(x, depth + 1));
return `[\n${items.join(",\n")}\n${pad(depth)}]`;
}
const obj = v as Record<string, unknown>;
const keys = Object.keys(obj).sort((a, b) => {
if (a === "kind") return b === "kind" ? 0 : -1;
if (b === "kind") return 1;
return a < b ? -1 : a > b ? 1 : 0;
});
if (keys.length === 0) return "{}";
const entries = keys.map((k) => `${pad(depth + 1)}${JSON.stringify(k)}: ${walk(obj[k], depth + 1)}`);
return `{\n${entries.join(",\n")}\n${pad(depth)}}`;
};
return walk(value, 0);
}
/** The non-negotiable doctrines the card must always carry (compactness never drops these). */
const NON_NEGOTIABLES: readonly { readonly rule: string; readonly detail: string }[] = [
{ rule: "Fail closed", detail: "a parse escape stands down; an unknown series de-arms with a logged reason — never a silent default." },
{ rule: "Never naked / bounded risk", detail: "a budget is a positive risk fraction (a type); a SELL is floored at intrinsic." },
{ rule: "Marks lie", detail: "a quote is a health signal, never a value: no active EXIT conditions on the mark — mid/bid/ask/last or the human words mark/premium — and mid is never a price anchor." },
{ rule: "Evidence honesty", detail: `the certified honest-tier cohort is ${CERTIFIED_COHORT}; a mechanical (practice) run is never labeled certified.` },
{ rule: "Text is a projection", detail: "the typed object model is canonical; `print(parse(text))` is a byte-stable fixed point (ADR-0004)." },
];
/** The fully-resolved, serializable card model — the single source both renderers consume. */
export interface CardModel {
readonly languageVersion: string;
readonly catalogHash: string;
readonly capabilities: readonly Capability[];
readonly surfaces: readonly AccessSurface[];
readonly rejects: readonly Capability[];
readonly examples: readonly ObjectTextExample[];
}
/** Build the resolved card model from the catalog + package version. Pure given its inputs. */
export function buildCardModel(
capabilities: readonly Capability[] = CAPABILITIES,
languageVersion: string = readLanguageVersion(),
): CardModel {
return {
languageVersion,
catalogHash: catalogHash(capabilities),
capabilities: cardCapabilities(capabilities),
surfaces: exposedSurfaces(capabilities),
rejects: rejects(capabilities),
examples: OBJECT_TEXT_EXAMPLES,
};
}
// ── Renderers (pure functions of the model) ──────────────────────────────────────
const cell = (s: string): string => s.replace(/\|/g, "\\|");
/** The four statement kinds (surfaces) of the language — a fixed pedagogical spine (CONTEXT.md). */
const STATEMENT_KINDS: readonly { readonly kind: string; readonly gloss: string }[] = [
{ kind: "VIEW", gloss: "what should I see? — panes at a token budget, materialized as a Frame" },
{ kind: "WAKE", gloss: "when should I look? — a standing subscription that spends attention, not risk" },
{ kind: "PLAN", gloss: "what may execute? — a bounded-risk contingent program the runtime fires" },
{ kind: "GRADE", gloss: "did it actually work? — an imperative replay reporting honest EV" },
];
/**
* Render the human Markdown card. Pure: byte-identical for a given model. Uses only tables and inline
* / `text` / `json` code fences (never a bare ```kestrel fence) so it adds NO managed fence to the
* gsh.1 corpus — its examples are governed by this card's own round-trip test instead.
*/
export function renderCardMarkdown(model: CardModel): string {
const out: string[] = [];
const L = (line = ""): void => void out.push(line);
L("<!-- GENERATED FILE — do not edit by hand.");
L(` Regenerate from the typed capability catalog with: ${REGEN_COMMAND}`);
L(" Source of truth: tests/support/catalog.ts · renderer: tests/support/card.ts");
L(" A CI test (tests/docs.card.test.ts) asserts this file equals a fresh regeneration,");
L(" that it fits the declared token budget, and that every object⇄text pair round-trips. -->");
L();
L("# Kestrel agent language card");
L();
L(
"A compact, receipt-backed reference for **authoring Kestrel** — one typed, token-efficient " +
"language for agentic trading with four statement kinds over one lexical core. This card is a " +
"**compiled projection of the capability catalog** (`tests/support/catalog.ts`), the same " +
"four-axis source of truth the [status page](./status.md) derives from. It claims nothing the " +
"catalog does not: every capability below is graded on syntax · runtime · evidence · access and " +
"backed by a source (`src/`) and test (`tests/`) receipt.",
);
L();
L(`- **Language version:** \`${model.languageVersion}\``);
L(`- **Catalog hash:** \`sha256:${model.catalogHash.slice(0, 16)}\` (full hash in \`card.json\`) — a drifted catalog invalidates this card.`);
L(`- **Token budget:** ≤ ${TOKEN_BUDGET} tokens under \`${TOKENIZER}\` (enforced in CI).`);
L(`- **Certified honest-tier cohort:** ${CERTIFIED_COHORT} — every \`mechanical (practice)\` row below is a mechanically-graded practice run, never certified.`);
L();
L("## Non-negotiables");
L();
L("Compactness never drops these — they are the doctrine the runtime enforces, not style.");
L();
for (const n of NON_NEGOTIABLES) L(`- **${n.rule}.** ${n.detail}`);
L();
L("## Statement kinds (surfaces)");
L();
L("| Kind | What it answers |");
L("| --- | --- |");
for (const s of STATEMENT_KINDS) L(`| \`${s.kind}\` | ${cell(s.gloss)} |`);
L();
L("## Capabilities — four axes");
L();
L(
"A feature can *parse* (syntax) yet have no *runtime*, no graded *evidence*, and only partial " +
"*access*. These four facts are independent; a single \"supported\" flag would collapse them into " +
"one flattering lie.",
);
L();
L("| Capability | id | Syntax | Runtime | Evidence | Access |");
L("| --- | --- | --- | --- | --- | --- |");
for (const c of model.capabilities) {
L(
`| ${cell(c.title)} | \`${c.id}\` | ${SYNTAX_LABEL[c.syntax.status]} | ${RUNTIME_LABEL[c.runtime.status]} | ${EVIDENCE_LABEL[c.evidence.tier]} | ${cell(accessString(c))} |`,
);
}
L();
L(`**Access surfaces exposed:** ${model.surfaces.map((s) => `\`${s}\``).join(" · ")}.`);
L();
L("## What each capability is");
L();
for (const c of model.capabilities) {
L(`- **${cell(c.title)}** (\`${c.id}\`) — ${cell(c.summary)}`);
}
L();
L("## Fail-closed rejects");
L();
L("These are refused at parse time — the illegal statement never reaches the engine.");
L();
L("| id | Doctrine |");
L("| --- | --- |");
for (const c of model.rejects) {
const note = c.syntax.note ?? c.summary;
L(`| \`${c.id}\` | ${cell(note)} |`);
}
L();
L("## Object ⇄ text (ADR-0004 round-trip)");
L();
L(
"The typed AST object **is** the language; text is a byte-stable projection of it. For every pair " +
"below `print(parse(text)) === text` (the card's test proves it). Objects are shown as JSON; the " +
"full objects for all pairs are in `card.json`.",
);
L();
for (const ex of model.examples) {
const { object, printed } = roundTrip(ex);
L(`### ${cell(ex.title)}`);
L();
L(ex.note);
L();
L("Canonical text:");
L();
L("```text");
L(printed);
L("```");
L();
if (ex.showObject) {
L("Object (parsed AST):");
L();
L("```json");
L(stableStringify(object));
L("```");
L();
}
}
L("## Provenance");
L();
L(
"Generated from `tests/support/catalog.ts` by `tests/support/card.ts`. Regenerate with " +
`\`${REGEN_COMMAND}\`. The committed card is asserted byte-identical to a fresh regeneration in ` +
"`tests/docs.card.test.ts`; if you change the catalog, regenerate or CI fails.",
);
L();
return out.join("\n") + "\n";
}
/**
* Render the machine card (card.json). Pure given `model` + the measured Markdown token count.
* `markdownTokens` is passed in (not recomputed here) so the two files are generated from one
* measurement and stay mutually consistent.
*/
export function renderCardJson(model: CardModel, markdownTokens: number): string {
const doc = {
$generated: {
note: "GENERATED FILE — do not edit by hand.",
regenerate: REGEN_COMMAND,
sourceOfTruth: "tests/support/catalog.ts",
renderer: "tests/support/card.ts",
checkedBy: "tests/docs.card.test.ts",
},
languageVersion: model.languageVersion,
catalogHash: `sha256:${model.catalogHash}`,
tokenizer: TOKENIZER,
tokenBudget: TOKEN_BUDGET,
markdownTokens,
certifiedCohort: CERTIFIED_COHORT,
evidenceSource: EVIDENCE_SOURCE,
nonNegotiables: NON_NEGOTIABLES.map((n) => ({ rule: n.rule, detail: n.detail })),
statementKinds: STATEMENT_KINDS.map((s) => ({ kind: s.kind, answers: s.gloss })),
surfaces: model.surfaces,
capabilities: model.capabilities.map((c) => ({
id: c.id,
title: c.title,
summary: c.summary,
syntax: { status: c.syntax.status, ...(c.syntax.note ? { note: c.syntax.note } : {}) },
runtime: { status: c.runtime.status, ...(c.runtime.note ? { note: c.runtime.note } : {}) },
evidence: { tier: c.evidence.tier, ...(c.evidence.note ? { note: c.evidence.note } : {}) },
access: {
surfaces: c.access.surfaces,
...(c.access.note ? { note: c.access.note } : {}),
},
receipts: { source: [...c.receipts.source].sort(), test: [...c.receipts.test].sort() },
examples: [...c.examples].sort(),
})),
rejects: model.rejects.map((c) => ({ id: c.id, doctrine: c.syntax.note ?? c.summary })),
objectText: model.examples.map((ex) => {
const { object, printed } = roundTrip(ex);
return { title: ex.title, note: ex.note, text: printed, object };
}),
};
return stableStringify(doc) + "\n";
}
/** Build both card files from the catalog + package version. The generator and tests share this. */
export function renderCard(
capabilities: readonly Capability[] = CAPABILITIES,
languageVersion: string = readLanguageVersion(),
): { markdown: string; json: string; markdownTokens: number; model: CardModel } {
const model = buildCardModel(capabilities, languageVersion);
const markdown = renderCardMarkdown(model);
const markdownTokens = countTokens(markdown);
const json = renderCardJson(model, markdownTokens);
return { markdown, json, markdownTokens, model };
}