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.
251 lines • 18.7 kB
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 { type BusEvent } from "../bus/index.ts";
import type { Blotter } from "../blotter/index.ts";
import type { CatalogEntry, CatalogListing, InputPin } from "../protocol/catalog.ts";
import type { Brief, Mandate } from "../frame/index.ts";
import { type AgentConfig, type TimeOfDay } from "../session/index.ts";
/** 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 declare const SESSION_CATALOG_VERSION = "1.1.0";
/** 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 declare class SessionCatalogError extends Error {
readonly code: SessionCatalogErrorCode;
constructor(code: SessionCatalogErrorCode, message: string);
}
/**
* 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 declare function planDocPin(text: string): string;
/**
* 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 declare function deriveInputPin(config: AgentConfig, planDoc: string): InputPin;
/**
* 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 declare function resolvePlanDoc(e: SessionCatalogEntry): string;
/**
* 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 declare function verifyPlanArtifact(e: SessionCatalogEntry): void;
/**
* 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 declare function loadSessionCatalog(manifest?: SessionCatalogManifest): readonly SessionCatalogEntry[];
/**
* 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 declare function catalogEntries(manifest?: SessionCatalogManifest): readonly CatalogEntry[];
/**
* 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 declare function resolveTapeEvents(e: SessionCatalogEntry): readonly BusEvent[];
/**
* 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 declare function runCatalogEntry(e: SessionCatalogEntry): Promise<{
readonly blotter: Blotter;
readonly conformanceRoot: string;
}>;
/**
* 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 declare function verifyCatalogEntry(e: SessionCatalogEntry): Promise<string>;
/**
* 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 declare const ENGINE_VERSION = "kestrel-engine/4";
/**
* 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 declare const JUDGE_VERSION = "kestrel-judge/1";
/**
* 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 declare const RECIPE_WAKE_COUNT: number;
export declare const SESSION_CATALOG: SessionCatalogManifest;
/**
* 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 declare function catalogListing(): readonly CatalogListing[];
//# sourceMappingURL=session-catalog.d.ts.map