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.
626 lines (585 loc) • 39.6 kB
text/typescript
/**
* # catalog/session-catalog — the public, versioned, content-addressed Session catalog (kestrel-djm.8)
*
* The publication-safe SYNTHETIC fixtures promoted into a VERSIONED, CONTENT-ADDRESSED public catalog: one
* {@link SESSION_CATALOG} manifest of generic Session artifacts a client can RUN LOCALLY end-to-end through
* djm.4's incremental Session controller ({@link openSession} → `start` → `advance` → `finalize` → Blotter →
* graded bus) to reproduce each entry's pinned conformance root, byte-for-byte. The SAME manifest is
* mirrorable by kestrel.markets WITHOUT rewriting, because every entry EMBEDS a djm.2 {@link CatalogEntry}
* ({@link ./protocol/catalog.ts} — the ONE catalog contract, never forked) verbatim as `.entry`, and the
* platform mirror consumes exactly {@link catalogEntries} — the same `CatalogEntry[]`, no rewrite.
*
* ## What each entry pins (djm.8 acceptance)
* - content roots (content-addressed): the input TAPE root (`entry.provenance.source`) and the graded-bus
* CONFORMANCE root (`sha256:<expectedConformanceRoot>`), both in `entry.contentRoots` (ADR-0010/0011: the
* graded bus is the recorded truth; a projection pins here).
* - provenance/rights + corpus tier: `entry.provenance` + `entry.corpusTier` (a PUBLIC tier).
* - engine/Judge compatibility: `entry.requirements` (a client whose engine/Judge drift MUST fail closed).
* - fill policy: `entry.permittedFillModel` (the single graded model — the two-bus rule).
* - View/Rendering identities: `entry.viewIdentity` / `entry.renderingIdentity` (content-addressed handles).
* - the local RUN RECIPE (`.recipe`) + the shipped generic tape (`.tape`) — the non-contract legs the LOCAL
* package runs on; the platform mirror never needs them (it resolves the content roots server-side).
*
* ## FAIL CLOSED (djm.8 AC3) — a typed refusal, never a silent load/run
* {@link loadSessionCatalog} / {@link runCatalogEntry} / {@link verifyCatalogEntry} refuse a corrupted
* content root (`corrupt-content-root`), an unsupported schema (`unsupported-schema`), or an unknown fill
* model (`unknown-fill-model`) with a typed {@link SessionCatalogError} — the entry is never partially loaded
* or run against a drifted implementation. {@link resolveTapeEvents} is a content-addressed resolver: a tape
* whose bytes no longer hash to `entry.provenance.source` is refused too.
*
* ## DETERMINISM (djm.8 AC2) — content-addressed, byte-stable
* The conformance root IS `blotter.session.bus.sha256` (= `SimRunId(gradedBus)`, run-identity.ts) — a pure,
* host-independent hash of the graded bus (no wall clock, no RNG on the record path). Same catalog + same
* engine ⇒ the same root, every run. {@link runCatalogEntry} drives the REAL controller (Bus→Blotter→Grade),
* so a catalog run is byte-identical to an independent `openSession → start → advance → finalize` drive.
*
* ── DESIGN FORK LOGGED (hard rules forbid .beads writes — recorded here like djm.4's 7dv.4 note) ──
* djm.2's {@link CatalogEntry} is FROZEN and carries neither the local run recipe nor a dedicated
* `expectedConformanceRoot`, so a bare `CatalogEntry[]` is not locally runnable. MINIMAL shape consistent
* with djm.2 + content-addressing: a versioned WRAPPER ({@link SessionCatalogManifest}) whose entries embed a
* `CatalogEntry` VERBATIM (`.entry`) alongside the execution `.recipe`, the shipped generic `.tape`, and the
* bare `.expectedConformanceRoot`. The graded-bus conformance root is ALSO pinned INSIDE the djm.2 entry as a
* content root (`sha256:<root>` ∈ `entry.contentRoots`), so it reconciles with djm.2's existing field without
* a second contract; the mirror view is `catalogEntries()` → the same `CatalogEntry[]`. Roots are regenerable
* via `scripts/catalog/compute-catalog-roots.ts`. Generic tickers only (SPX/SPY/QQQ), ARCHITECTURE §7.
*/
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { readBus, serializeBus, type BusEvent } from "../bus/index.ts";
import { sha256 } from "../crypto/sha256.ts";
import type { Blotter } from "../blotter/index.ts";
import type { CatalogEntry, CatalogListing, InputPin } from "../protocol/catalog.ts";
import { buildCatalogListing } from "./catalog-listing.ts";
import type { Brief, Mandate } from "../frame/index.ts";
import { PROTOCOL_VERSION } from "../protocol/index.ts";
import {
BACKTEST_CONFIG,
cellConfigId,
declaresEnvelope,
openSession,
type AgentConfig,
type FillModelName,
type SessionController,
type TimeOfDay,
} from "../session/index.ts";
// ─────────────────────────────────────────────────────────────────────────────
// Public surface — the versioned manifest + its embedded-entry wrapper
// ─────────────────────────────────────────────────────────────────────────────
/** The local RUN RECIPE the LOCAL package drives an entry on — the legs djm.2's frozen {@link CatalogEntry}
* deliberately does not carry (it pins reproducibility facts, not an executable script). Passed VERBATIM to
* {@link openSession}; the fill model rides {@link CatalogEntry.permittedFillModel} (the one graded model), so
* it is not duplicated here. */
export interface SessionRecipe {
/** The initial armed Kestrel document authored at OPEN (a NO-LLM Backtest: arm once, pass forever). */
readonly document: string;
/** The authored wake vantages (ET times-of-day), strictly inside the driven window. */
readonly wakes: readonly TimeOfDay[];
/** The dollar value of one risk unit `R` for this grade. */
readonly rUsd: number;
/** The cell's hard {@link Mandate} (ADR-0026) — objective + R-definition + success criterion +
* bounded-risk rule the agent is graded against. Rendered in the kernel; the ONLY admission input.
* OPTIONAL here (a NO-LLM backtest entry declares none); a GRADED LLM run MUST declare one. */
readonly mandate?: Mandate;
/** The cell's soft {@link Brief} (ADR-0026) — directional English (goal/approach/persona), rendered
* as standing context + hashed + attributed, but NEVER an admission input (the hard guard). */
readonly brief?: Brief;
/** The injected constant fair-tau (years) function — a documented sim simplification. */
readonly fairTauYears: (now: number) => number | null;
}
/**
* One public Session catalog entry: a djm.2 {@link CatalogEntry} embedded VERBATIM (`.entry` — the mirrorable
* contract) alongside the shipped generic `.tape`, the local `.recipe`, and the bare `.expectedConformanceRoot`
* (which is ALSO pinned as `sha256:<root>` inside `entry.contentRoots`). `id` is a stable, human-readable
* identifier the executable examples / paper reproduction / practice benchmarks share.
*/
export interface SessionCatalogEntry {
/** Stable content-addressable-independent identifier (shared across examples/paper/benchmarks). */
readonly id: string;
/** The djm.2 catalog contract, embedded verbatim (the ONE contract — never forked). */
readonly entry: CatalogEntry;
/** The shipped generic tape fixture basename (resolved under `fixtures/`, content-addressed to
* `entry.provenance.source`). */
readonly tape: string;
/** The local execution recipe (djm.2's `CatalogEntry` carries no runnable script). */
readonly recipe: SessionRecipe;
/** The pinned graded-bus conformance root (a bare 64-hex sha256; also in `entry.contentRoots`). */
readonly expectedConformanceRoot: string;
}
/** The VERSIONED manifest: a version tag + the public entries. The version is bumped on any change to the
* pinned roots / recipes / entry set, so a consumer can pin an exact catalog revision. */
export interface SessionCatalogManifest {
readonly version: string;
readonly entries: readonly SessionCatalogEntry[];
}
/** The catalog schema version (independent of {@link PROTOCOL_VERSION}, which the embedded djm.2 entries
* track). Bumped when the pinned roots / entries change.
*
* Bumped 1.0.0 → 1.1.0 (kestrel-a57.18): ADDITIVE, schema-stable re-pin. The graded session bus now carries
* one agent-decision RECEIPT — a CONTROL `propose` per armed PLAN — so the restraint benchmark's `grade/receipts`
* projector is non-empty on a real session (it was INERT in production before). No wire field was added or
* removed; only the pinned conformance roots moved, by EXACTLY that +1 receipt-per-PLAN event (verified: stripping
* the propose events reproduces the pre-change roots byte-for-byte). A MINOR bump is the honest SemVer signal for
* a backward-additive change with an unchanged schema. Pre-launch — nobody had pinned 1.0.0 yet. No ADR mandates a
* specific catalog-version policy (rg over docs/adr found none), so SemVer-additive was applied; the engine-epoch
* bump that the root move requires is {@link ENGINE_VERSION} kestrel-engine/2 → /3 below. */
export const SESSION_CATALOG_VERSION = "1.1.0";
// ─────────────────────────────────────────────────────────────────────────────
// Fail-closed error surface (djm.8 AC3)
// ─────────────────────────────────────────────────────────────────────────────
/** The typed refusal codes: a corrupted/mismatched content root, an unsupported schema, an unknown fill
* model — the three fail-closed cases djm.8 mandates — plus the two m9i.27 plan-artifact refusals: a
* fixed-plan cell whose pinned doc is MISSING (`plan-doc-missing`) or whose stored doc does NOT hash to its
* pin (`plan-doc-mismatch`). A plan artifact gets its OWN typed refusal, distinct from a tape's
* `corrupt-content-root`. */
export type SessionCatalogErrorCode =
| "corrupt-content-root"
| "unsupported-schema"
| "unknown-fill-model"
| "plan-doc-missing"
| "plan-doc-mismatch";
/** A typed refusal from the Session catalog. Carries a {@link SessionCatalogErrorCode} so a caller can branch
* fail-closed without string-matching a message (never a silent load/run). */
export class SessionCatalogError extends Error {
readonly code: SessionCatalogErrorCode;
constructor(code: SessionCatalogErrorCode, message: string) {
super(message);
this.name = "SessionCatalogError";
this.code = code;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Content-addressing helpers (pure, deterministic — no wall clock, no RNG)
// ─────────────────────────────────────────────────────────────────────────────
const HEX64 = /^[0-9a-f]{64}$/;
/**
* The supported catalog wire schema for an embedded djm.2 entry (e.g. `kestrel.session/0.2`). A different
* schema fails closed as `unsupported-schema`.
*
* HONEST COUPLING (kestrel-b83) — this INTENTIONALLY and correctly tracks {@link PROTOCOL_VERSION}; it is NOT
* the 2t0 silent-drift bug that {@link ENGINE_VERSION} / {@link JUDGE_VERSION} froze. Those are grade/replay
* EPOCHS whose behaviour can change independently of the wire minor, so they must be frozen. This tag, by
* contrast, IS the wire-protocol shape: `kestrel.session/<minor>` names the exact `CatalogEntry` shape a client
* parses, so a session-schema change IS a protocol-minor change by definition — the two CANNOT move
* independently, and freezing this to an epoch decoupled from the protocol would be the dishonest move (a
* schema tag diverging from the protocol it names). It therefore correctly tracks PROTOCOL_VERSION, and AC1
* pins `entry.schema === kestrel.session/${PROTOCOL_VERSION}`. The only b83 gap was that this coupling was
* SILENT; this comment makes it explicit.
*/
const SUPPORTED_SCHEMA = `kestrel.session/${PROTOCOL_VERSION}`;
/** The known fill models an entry may be graded under (RUNTIME §6). Any other name fails closed as
* `unknown-fill-model` — the entry is never run against an unknown model. */
const KNOWN_FILL_MODELS: readonly FillModelName[] = ["strict-cross-v1", "maker-fair-v1"];
/** The content address of a bus (tape or graded): the codebase's ONE bus serializer → the ONE portable
* sha256 (`SimRunId`/`blotter.session.bus.sha256` use the identical convention), so a tape root here and a
* conformance root there are the same kind of content id. */
function busRoot(events: readonly BusEvent[]): string {
return sha256(serializeBus(events));
}
/** Strip the `sha256:` prefix off a djm.2 {@link import("../protocol/catalog.ts").ContentRoot} handle, or
* `null` when it is not a well-formed `sha256:<64-hex>` address. */
function bareRootOf(handle: string): string | null {
const m = /^sha256:([0-9a-f]{64})$/.exec(handle);
return m === null ? null : m[1]!;
}
function msgOf(e: unknown): string {
return e instanceof Error ? e.message : String(e);
}
/**
* The absolute path to a shipped tape fixture, resolved relative to THIS module — so it resolves
* whether the package is consumed from `src/` under Bun or from the bundled `dist/` under node.
*
* `fileURLToPath(import.meta.url)`, NOT `import.meta.dir`: the latter is a Bun-ism that
* `bun build --target=node` passes through VERBATIM, and under node it is `undefined`. That made
* `join(undefined, …)` throw a raw TypeError deep inside the catalog — surfacing to an agent as
* `corrupt-content-root` ("your content root is corrupt") when the truth was "this host has no Bun"
* (kestrel-mkn review B1). The node-portable form is the only one that can be trusted in shipped code.
*/
function fixturePath(tape: string): string {
return join(dirname(fileURLToPath(import.meta.url)), "fixtures", tape);
}
// ─────────────────────────────────────────────────────────────────────────────
// Plan-doc / config INPUT PIN — content-addressed, recoverable, fail-closed (kestrel-m9i.27)
// ─────────────────────────────────────────────────────────────────────────────
/**
* Content-address a FIXED plan doc: `sha256:` + the doc's OWN sha256 — the run-identity/SimRunId
* convention (`busRoot` / `deriveConfigId` use the identical one sha256), NOT a name. Deterministic: the
* SAME doc ⇒ the SAME pin; a CHANGED doc ⇒ a CHANGED pin — so a plan-doc difference is VISIBLE in the pin
* (two different docs authored under one plan NAME get different pins, unlike the name-only `plan_instance`
* id that collides them). Pure — no wall clock, no RNG.
*/
export function planDocPin(text: string): string {
return `sha256:${sha256(text)}`;
}
/**
* Derive the {@link InputPin} a cell pins from its run config + authored doc (kestrel-m9i.27). Discriminates
* on {@link declaresEnvelope} — the SAME split the run-identity cell axis uses, so the pin and the cell axis
* never drift:
* - a NO-LLM Backtest declares no a57.14 envelope ⇒ there IS a fixed plan doc ⇒ pin the DOC
* ({@link planDocPin} — recoverable byte-for-byte);
* - a live-authoring config declares an envelope ⇒ the agent authors the plan LIVE ⇒ pin the CONFIG
* (the {@link cellConfigId cell ConfigId} as a content root — the reproducible input is the config, not
* a doc).
* Pure — no wall clock, no RNG.
*/
export function deriveInputPin(config: AgentConfig, planDoc: string): InputPin {
if (declaresEnvelope(config)) {
return { kind: "agent-authored", config: `sha256:${cellConfigId(config)}` };
}
return { kind: "fixed-plan", planDoc: planDocPin(planDoc) };
}
/**
* Recover a fixed-plan cell's EXACT authored plan doc from the manifest, content-address VERIFIED: the
* stored doc (`e.recipe.document` — the content-addressed store the LOCAL package ships) is returned ONLY
* when its sha256 equals the pinned `planDoc` hash, so the returned string is BYTE-FOR-BYTE the authored
* plan the run drove on. A pin that is missing or agent-authored (no recoverable doc) fails closed as
* `plan-doc-missing`; a stored doc that does NOT hash to its pin fails closed as `plan-doc-mismatch` —
* never a silently-unverified doc (a benchmark cell is never treated as reproducible on trust). Pure.
*/
export function resolvePlanDoc(e: SessionCatalogEntry): string {
const pin = e.entry.inputPin;
if (pin === undefined || pin.kind !== "fixed-plan") {
throw new SessionCatalogError(
"plan-doc-missing",
`entry ${e.id}: no fixed-plan doc to resolve (${pin === undefined ? "no inputPin" : `pin kind ${pin.kind}`})`,
);
}
const doc = e.recipe.document;
const expected = bareRootOf(pin.planDoc);
const actual = sha256(doc);
if (expected === null || actual !== expected) {
throw new SessionCatalogError(
"plan-doc-mismatch",
`entry ${e.id}: stored plan doc (sha256 ${actual}) does not match pinned ${JSON.stringify(pin.planDoc)}`,
);
}
return doc;
}
/**
* Fail-closed verification of a cell's plan artifact (kestrel-m9i.27): a fixed-plan cell MUST pin its doc
* AND the stored doc MUST hash to the pin; an agent-authored cell MUST pin a well-formed config content
* root. A MISSING pin, a MISMATCHED doc, or a malformed config pin throws a typed {@link SessionCatalogError}
* (`plan-doc-*`) — the cell is flagged incomplete, NEVER silently treated as reproducible. A shipped
* {@link SessionCatalogEntry} drives a NO-LLM Backtest, so it MUST carry a fixed-plan pin whose doc is
* recoverable. Pure — no side effects, no wall clock.
*/
export function verifyPlanArtifact(e: SessionCatalogEntry): void {
const pin = e.entry.inputPin;
if (pin === undefined) {
throw new SessionCatalogError(
"plan-doc-missing",
`entry ${e.id}: fixed-plan cell carries no input pin — its authored plan doc is unrecoverable (fail closed)`,
);
}
if (pin.kind === "fixed-plan") {
resolvePlanDoc(e); // re-hash the stored doc against the pin (throws plan-doc-mismatch on any drift)
return;
}
// agent-authored: no fixed doc — the reproducible input is the CONFIG; verify it is a real content root.
if (bareRootOf(pin.config) === null) {
throw new SessionCatalogError(
"plan-doc-mismatch",
`entry ${e.id}: agent-authored config pin ${JSON.stringify(pin.config)} is not a sha256 content root`,
);
}
}
/**
* Fail-closed structural validation of ONE entry, independent of running it: the embedded schema is
* supported, the fill model is known, the pinned conformance root is a bare 64-hex that is ALSO
* content-addressed into `entry.contentRoots` (the graded bus is the recorded truth), AND the m9i.27 plan
* artifact is present + content-verified ({@link verifyPlanArtifact}). Throws a typed
* {@link SessionCatalogError} on the first violation — the entry is never partially accepted.
*/
function assertEntryValid(e: SessionCatalogEntry): void {
if (e.entry.schema !== SUPPORTED_SCHEMA) {
throw new SessionCatalogError(
"unsupported-schema",
`entry ${e.id}: schema ${JSON.stringify(e.entry.schema)} is not the supported ${JSON.stringify(SUPPORTED_SCHEMA)}`,
);
}
if (!KNOWN_FILL_MODELS.includes(e.entry.permittedFillModel as FillModelName)) {
throw new SessionCatalogError(
"unknown-fill-model",
`entry ${e.id}: permittedFillModel ${JSON.stringify(e.entry.permittedFillModel)} is not a known fill model`,
);
}
if (!HEX64.test(e.expectedConformanceRoot)) {
throw new SessionCatalogError(
"corrupt-content-root",
`entry ${e.id}: expectedConformanceRoot ${JSON.stringify(e.expectedConformanceRoot)} is not a 64-hex sha256`,
);
}
if (!e.entry.contentRoots.includes(`sha256:${e.expectedConformanceRoot}`)) {
throw new SessionCatalogError(
"corrupt-content-root",
`entry ${e.id}: conformance root sha256:${e.expectedConformanceRoot} is not pinned in entry.contentRoots`,
);
}
// m9i.27: the plan artifact must be pinned + content-verified — a fixed-plan cell whose authored doc is
// missing or does not hash to its pin is refused here (never silently reproducible), on BOTH the load path
// and the run path (both flow through assertEntryValid).
verifyPlanArtifact(e);
}
// ─────────────────────────────────────────────────────────────────────────────
// Load / mirror (djm.8 AC1)
// ─────────────────────────────────────────────────────────────────────────────
/**
* Fail-closed load + validate a manifest (the shipped {@link SESSION_CATALOG} by default): every entry is
* structurally validated ({@link assertEntryValid}) AND its tape is content-address-verified
* ({@link resolveTapeEvents}) before any is returned — a corrupt/unsupported/unknown entry throws a typed
* {@link SessionCatalogError}, never a partial load. Returns the validated {@link SessionCatalogEntry} list.
*/
export function loadSessionCatalog(
manifest: SessionCatalogManifest = SESSION_CATALOG,
): readonly SessionCatalogEntry[] {
for (const e of manifest.entries) {
assertEntryValid(e);
resolveTapeEvents(e); // content-address the tape (throws corrupt-content-root on a byte mismatch)
}
return manifest.entries;
}
/**
* The platform-mirror view (djm.8 AC1 / AC5): the validated manifest projected to the SAME djm.2
* `CatalogEntry[]` the managed backend consumes — no rewrite, no second contract. kestrel.markets mirrors
* exactly this.
*/
export function catalogEntries(
manifest: SessionCatalogManifest = SESSION_CATALOG,
): readonly CatalogEntry[] {
return loadSessionCatalog(manifest).map((e) => e.entry);
}
// ─────────────────────────────────────────────────────────────────────────────
// Content-addressed tape resolver (djm.8 AC3/AC4)
// ─────────────────────────────────────────────────────────────────────────────
/**
* Resolve an entry's shipped generic tape to its {@link BusEvent} stream, content-addressed: the tape bytes
* are read + re-serialized canonically, and the root MUST equal `entry.provenance.source` — a mismatch (a
* corrupted or swapped tape) fails closed as `corrupt-content-root`, never a silent load. Pure + deterministic.
*/
export function resolveTapeEvents(e: SessionCatalogEntry): readonly BusEvent[] {
let events: readonly BusEvent[];
try {
events = [...readBus(fixturePath(e.tape))];
} catch (err) {
throw new SessionCatalogError("corrupt-content-root", `entry ${e.id}: tape ${e.tape} is unreadable (${msgOf(err)})`);
}
const expected = bareRootOf(e.entry.provenance.source);
const actual = busRoot(events);
if (expected === null || actual !== expected) {
throw new SessionCatalogError(
"corrupt-content-root",
`entry ${e.id}: tape ${e.tape} content root ${actual} does not match pinned ${e.entry.provenance.source}`,
);
}
return events;
}
// ─────────────────────────────────────────────────────────────────────────────
// Run / verify one entry through djm.4's REAL Session controller (djm.8 AC2)
// ─────────────────────────────────────────────────────────────────────────────
/** Drive ONE entry end-to-end through djm.4's incremental controller: OPEN → author the recipe document →
* pass every delivered Wake → finalize. IDENTICAL sequence to a bare fixed-plan drive, so a catalog run is
* byte-identical to an independent `openSession → start → advance → finalize`. */
async function driveEntry(e: SessionCatalogEntry, events: readonly BusEvent[]): Promise<Blotter> {
const ctrl: SessionController = openSession({
events,
config: BACKTEST_CONFIG,
wakes: e.recipe.wakes,
fillModel: e.entry.permittedFillModel as FillModelName,
rUsd: e.recipe.rUsd,
...(e.recipe.mandate !== undefined ? { mandate: e.recipe.mandate } : {}),
...(e.recipe.brief !== undefined ? { brief: e.recipe.brief } : {}),
fairTauYears: e.recipe.fairTauYears,
});
await ctrl.start();
let res = await ctrl.advance({ kind: "authored", document: e.recipe.document });
if (!res.ok) {
throw new SessionCatalogError("corrupt-content-root", `entry ${e.id}: initial authoring refused (${res.diagnostic})`);
}
while (res.ok && res.next !== null) {
res = await ctrl.advance({ kind: "pass" });
if (!res.ok) {
throw new SessionCatalogError("corrupt-content-root", `entry ${e.id}: wake advance refused (${res.diagnostic})`);
}
}
return (await ctrl.finalize()).blotter;
}
/**
* Run ONE entry LOCALLY end-to-end through the REAL incremental Session path (djm.4's controller over
* `runSimulateSession` → Bus → Blotter → Grade) and return its graded {@link Blotter} + `conformanceRoot`
* (= `blotter.session.bus.sha256` = `SimRunId`). Fail-closed FIRST ({@link assertEntryValid} +
* {@link resolveTapeEvents}), so an unknown fill model / corrupted root / unsupported schema is refused before
* anything runs. Deterministic — same entry + same engine ⇒ the same root.
*/
export async function runCatalogEntry(
e: SessionCatalogEntry,
): Promise<{ readonly blotter: Blotter; readonly conformanceRoot: string }> {
assertEntryValid(e);
const events = resolveTapeEvents(e);
const blotter = await driveEntry(e, events);
return { blotter, conformanceRoot: blotter.session.bus.sha256 };
}
/**
* Run ONE entry and VERIFY its produced conformance root against the manifest's pinned root, fail-closed: a
* drift (the produced root differs from `expectedConformanceRoot`, or is not pinned in `entry.contentRoots`)
* throws a typed `corrupt-content-root`. Returns the verified root on success. This is the reproducibility
* contract a client checks before trusting a grade.
*
* HONEST DRIFT (kestrel-2t0): the pins are golden-like ("as of {@link ENGINE_VERSION}, entry E ⇒ root Y"), so
* a produced-root mismatch is the SAME signal as an engine change that outran the frozen epoch. Its message is
* therefore ACTIONABLE — it names ENGINE_VERSION (bump it) and `scripts/catalog/compute-catalog-roots.ts`
* (regenerate the pins) — rather than a bare hash mismatch that reads as a mysterious CI red. The
* determinism-only guard (run each entry twice ⇒ identical root, tests/catalog.session.test.ts) covers the
* churn-robust reproducibility question separately, so this exact check can stay strict without being brittle
* to an intended engine change.
*/
export async function verifyCatalogEntry(e: SessionCatalogEntry): Promise<string> {
const { conformanceRoot } = await runCatalogEntry(e);
if (conformanceRoot !== e.expectedConformanceRoot) {
throw new SessionCatalogError(
"corrupt-content-root",
`entry ${e.id}: produced conformance root ${conformanceRoot} does not match the root pinned as of ` +
`ENGINE_VERSION ${ENGINE_VERSION} (${e.expectedConformanceRoot}). If this is an INTENDED engine change, ` +
`bump ENGINE_VERSION in src/catalog/session-catalog.ts and regenerate the pins via ` +
`'bun scripts/catalog/compute-catalog-roots.ts' (then re-bake the mirror via ` +
`'bun scripts/sdk/bake-catalog-records.ts'); otherwise the engine is non-reproducible (fail closed).`,
);
}
if (!e.entry.contentRoots.includes(`sha256:${conformanceRoot}`)) {
throw new SessionCatalogError(
"corrupt-content-root",
`entry ${e.id}: produced conformance root sha256:${conformanceRoot} is not pinned in entry.contentRoots`,
);
}
return conformanceRoot;
}
// ─────────────────────────────────────────────────────────────────────────────
// The manifest — generic synthetic Session artifacts (QQQ; no leak)
// ─────────────────────────────────────────────────────────────────────────────
/** The generic Backtest recipe every catalog entry runs: arm one at-the-money long call at OPEN, then pass
* every Wake (a NO-LLM Backtest — {@link BACKTEST_CONFIG}). Two vantages strictly inside the ~08:30–09:29 ET
* window; a constant 6.5h fair-tau so `@fair`/`lean` build (a documented sim simplification, RUNTIME §4). */
const RECIPE: SessionRecipe = {
document: ["PLAN atm-rider budget 0.5R ttl +60m", " WHEN phase open", " DO buy 1 atm C @ lean(bid, fair, 0.5)"].join("\n"),
wakes: [
{ hour: 8, minute: 50 },
{ hour: 9, minute: 10 },
],
rUsd: 10_000,
fairTauYears: (): number => 6.5 / (365 * 24),
};
/** The ISO-8601 instant the catalog roots were recorded (a fixed, deterministic stamp — never a wall clock). */
const RECORDED_AT = "2026-07-13T00:00:00.000Z";
/** A PUBLIC corpus tier, aligned with the bus `corpus_tier` vocabulary (AC4: never a private/licensed tier). */
const CORPUS_TIER = "public-baseline";
/**
* The FROZEN engine epoch a faithful replay must satisfy (kestrel-2t0). Deliberately an explicit INTEGER
* epoch (`kestrel-engine/<N>`), DECOUPLED from {@link PROTOCOL_VERSION} (a wire
* `major.minor`): the per-entry `expectedConformanceRoot` pins are GOLDEN-LIKE — "as of THIS engine, entry E
* reproduces to root Y" — so a root-drifting fill/grade/settle change is an EXPLICIT epoch bump HERE, never a
* silent drift behind an unchanged version string (the pre-2t0 bug: `kestrel-engine/${PROTOCOL_VERSION}` moved
* the root while the string stayed `kestrel-engine/0.2`). Exported so the pins are honestly inspectable as "as
* of ENGINE_VERSION X"; the drift path in {@link verifyCatalogEntry} names it. Bump on any change that moves a
* pinned root, then regenerate the pins via `scripts/catalog/compute-catalog-roots.ts` and re-bake the mirror
* via `scripts/sdk/bake-catalog-records.ts`.
*/
export const ENGINE_VERSION = "kestrel-engine/4"; // epoch 4 (RESTATED 2026-07-18, kestrel-ukwz / ADR-0037 §2): @fair now derives the forward from put-call parity (F* = K* + (Cmid − Pmid)) instead of assuming forward = spot, threaded into BOTH the IV inversion and the Black-76 price. Epoch 4 additionally carries the fail-closed parity/fair-vs-book guards (kestrel-ku99) and a tau derived from the CONTRACT expiry (kestrel-wcnd); both are INERT on these 0DTE fixtures (expiry == session_date, and the parity band admits every strike here), so neither moves a root on its own. This is BENCHMARK-AFFECTING — it moves every fair-priced fill on a real option book. Only the choppy-1101-strict conformance root drifted (87dbb000…181b7 → 6b523f15…f35d1); spike/trending fills do not touch the forward wedge, so their roots are byte-unchanged. Recomputed via `bun scripts/catalog/compute-catalog-roots.ts` on the corrected engine and re-baked into the mirror via `bun scripts/sdk/bake-catalog-records.ts` — no root hand-edited. Bumped (never a silent drift behind /3) per the "Bump on any change that moves a root" rule below. // epoch 3: the agent-decision RECEIPT producer (kestrel-a57.18) — the driver now writes one CONTROL `propose` per armed PLAN onto the graded session bus (the grade/receipts projector was INERT in production before), so every graded-bus conformance root below moved by EXACTLY that +1 receipt event per PLAN. Schema UNCHANGED (thesis is the honest UNSTATED encoding); tape roots unchanged. Verified: stripping the propose events + re-densifying seq reproduces the epoch-2 roots byte-for-byte. // epoch 2: bus_schema v6 — the graded META advertises the clock-honest deliberation record (ADR-0040 / kestrel-w7la.1); the graded-bus conformance roots below moved with the META bytes (tape roots unchanged)
/**
* The FROZEN Judge (grade-engine) epoch a faithful replay must satisfy (kestrel-b83, mirroring
* {@link ENGINE_VERSION}). Deliberately an explicit INTEGER epoch (`kestrel-judge/<N>`), DECOUPLED from
* {@link PROTOCOL_VERSION} (a wire `major.minor`): a Judge BEHAVIOUR change (grade algorithm,
* floor/expected/realized channels, gating) is NOT a wire-protocol minor bump, so it MUST be an EXPLICIT
* epoch bump HERE, never a silent drift behind an unchanged version string (the pre-b83 bug:
* `kestrel-judge/${PROTOCOL_VERSION}` would let a Judge change land while the string stayed
* `kestrel-judge/0.2`). Exported so `entry.requirements.judgeVersion` is honestly inspectable as "graded under
* Judge epoch N". Bump on any Judge-behaviour change, then re-bake the mirror via
* `scripts/sdk/bake-catalog-records.ts`.
*/
export const JUDGE_VERSION = "kestrel-judge/1";
/** The Backtest View / Rendering identities — content-addressed handles over stable generic descriptors (a
* NO-LLM Backtest perceives the machine Frame under one fixed identity). */
const VIEW_IDENTITY = `sha256:${sha256(`kestrel.view/backtest/${PROTOCOL_VERSION}`)}`;
const RENDERING_IDENTITY = `sha256:${sha256(`kestrel.rendering/machine-frame/${PROTOCOL_VERSION}`)}`;
/** Assemble one entry from its generic tape + the pinned tape/conformance roots (regenerated by
* `scripts/catalog/compute-catalog-roots.ts`). Both roots are content-addressed into the embedded djm.2
* entry's `contentRoots`; the tape root is also its `provenance.source`. */
function makeEntry(args: {
readonly id: string;
readonly tape: string;
readonly fillModel: FillModelName;
readonly tapeRoot: string;
readonly conformanceRoot: string;
}): SessionCatalogEntry {
const entry: CatalogEntry = {
// The addressing handle rides on the wire entry too (kestrel-33yr.2): the SAME id as the wrapping
// SessionCatalogEntry, so a client that only ever sees the mirrored `catalogEntries()` projection can
// open the subject by `entry.id` — the discover→open loop closes with no reach into SESSION_CATALOG.
id: args.id,
contentRoots: [`sha256:${args.tapeRoot}`, `sha256:${args.conformanceRoot}`],
schema: SUPPORTED_SCHEMA,
corpusTier: CORPUS_TIER,
requirements: { engineVersion: ENGINE_VERSION, judgeVersion: JUDGE_VERSION },
permittedFillModel: args.fillModel,
viewIdentity: VIEW_IDENTITY,
renderingIdentity: RENDERING_IDENTITY,
provenance: { source: `sha256:${args.tapeRoot}`, recordedAt: RECORDED_AT },
// m9i.27: a NO-LLM Backtest (BACKTEST_CONFIG declares no envelope) ⇒ pin the FIXED plan doc by content
// hash. The doc itself ships content-addressed on `.recipe.document` (resolvePlanDoc recovers it
// byte-for-byte, re-hashed against this pin).
inputPin: deriveInputPin(BACKTEST_CONFIG, RECIPE.document),
};
return { id: args.id, entry, tape: args.tape, recipe: RECIPE, expectedConformanceRoot: args.conformanceRoot };
}
/**
* The public Session catalog: one generic synthetic tape per market regime (choppy / spike / trending, all
* QQQ), graded across both shipped fill models. Each entry RUNS locally to its pinned conformance root through
* djm.4's controller ({@link runCatalogEntry}) and is mirrorable by the platform as {@link catalogEntries}.
*/
/**
* The number of decision Wake frames every catalog subject presents — the honest {@link CatalogListing}
* `frameCount` (kestrel-dnxq). Single-sourced from {@link RECIPE} here (the heavy, engine-side authority) and
* re-exported so the LIGHT discovery-metadata table (`src/catalog/catalog-listing.ts`, which cannot import the
* engine-bearing RECIPE) is guarded against drift by tests/sdk.catalog-equal-projection.test.ts (`WAKE_FRAMES`
* there MUST equal this).
*/
export const RECIPE_WAKE_COUNT = RECIPE.wakes.length;
export const SESSION_CATALOG: SessionCatalogManifest = {
version: SESSION_CATALOG_VERSION,
entries: [
makeEntry({
id: "choppy-1101-strict",
tape: "choppy-1101.jsonl",
fillModel: "strict-cross-v1",
tapeRoot: "7166b08f3438c8ed6ee23b5e86ae785736f94870207a63051893a00f08b462b1",
// RESTATED 2026-07-18 (kestrel-ukwz, ADR-0037 §2/Consequences "BENCHMARK-AFFECTING"): the @fair
// parity-implied forward (F* = K* + (Cmid − Pmid)) moved the strict-cross fill on this choppy tape's
// fair-priced OTM-put leg, so the graded-bus conformance root drifted 87dbb000…181b7 → 6b523f15…f35d1.
// Recomputed by `bun scripts/catalog/compute-catalog-roots.ts` on the corrected engine (not hand-edited);
// tape root unchanged; spike/trending roots unchanged (their fills do not touch the forward wedge).
conformanceRoot: "6b523f157a0810ae97375e2a913906899e04f4f74f79157054d16829079f35d1",
}),
makeEntry({
id: "spike-1102-maker-fair",
tape: "spike-1102.jsonl",
fillModel: "maker-fair-v1",
tapeRoot: "5b50609e931959aa911b1c576a94d81c0947ff1e0193de8a40054de969baa583",
conformanceRoot: "20a1ac0ea1264b3f02449e81ee5d8d31afdd6439d6bc2629e7d56cbdf4bffb5b",
}),
makeEntry({
id: "trending-1103-maker-fair",
tape: "trending-1103.jsonl",
fillModel: "maker-fair-v1",
tapeRoot: "2528243e6a4b708bf169beffe6352b95e4db0e3374891e0ab78bfee8a7b871f2",
conformanceRoot: "fcd23dc97426d2c082e95fc1f86e0ff94fa0c601fd3836c000add10085f494bd",
}),
],
};
/**
* The catalog DISCOVERY projection (kestrel-dnxq) — the `catalog` op's canonical {@link CatalogListing}[], the
* offline in-process sample this transport lists. One row per {@link SESSION_CATALOG} entry, built through the
* SHARED {@link buildCatalogListing} constructor (the SINGLE source of the discovery metadata, in the light
* `catalog-listing.ts` module — so the LOCAL projection and the light contract server's `GET /catalog` can
* never drift). Distinct from {@link catalogEntries} (the reproducibility-complete djm.2 surface resolved at
* open); an id with no listing metadata fails closed there (never a silently blank listing row).
*/
export function catalogListing(): readonly CatalogListing[] {
return buildCatalogListing(SESSION_CATALOG.entries.map((e) => e.id));
}