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.
120 lines • 8.02 kB
TypeScript
/**
* # catalog/catalog-listing — the ONE canonical CATALOG DISCOVERY projection (kestrel-dnxq)
*
* The `catalog` op is subject-DISCOVERY: it returns a {@link CatalogListing}[] — the human-legible menu a
* client browses to choose WHAT to open. Before dnxq the two transports served DISJOINT shapes for it: the
* LOCAL transport dumped the reproducibility-complete djm.2 {@link CatalogEntry} (camelCase content roots /
* engine versions / View identities), while the managed backend's `GET /catalog` shipped a snake_case listing
* row (`artifact_id`, `frame_count`, `title`, …) with NO replay pins. Only `id` was shared, and cross-transport
* ids did not resolve — a headline invariant ("the protocol objects are byte-identical across transports") was
* false for a core op.
*
* This module is the single seam that makes the catalog op an EQUAL PROJECTION. BOTH transports build their
* rows through {@link toCatalogListing}, which fixes the canonical key ORDER, so a local row and a
* remote-decoded row for the same subject are BYTE-identical (not merely structurally equal). The managed
* backend's wire listing is mapped here ({@link decodeCatalogListingWire}: `artifact_id → id`,
* `frame_count → frameCount`, drop the `content_hash` replay pin) — a FAITHFUL rename of the same semantic
* fields, never a fabricated value; a row that carries no addressable handle fails closed
* ({@link CatalogListingDecodeError}) rather than returning an un-openable entry.
*
* ZERO heavy deps: pure data + pure functions over protocol type-only imports, so the light HTTP transport,
* the light contract server, and the (heavy) local loader all share it without pulling the engine.
*/
import type { CatalogListing, CatalogPage, CatalogPricing } from "../protocol/catalog.ts";
/** A fail-closed decode error — the managed `GET /catalog` body was not a listing, or a row carried no
* addressable handle. Never returns an un-openable entry. */
export declare class CatalogListingDecodeError extends Error {
constructor(message: string);
}
/** The DENORMALISED fields a caller supplies to build a canonical listing row. */
export interface CatalogListingFields {
readonly id: string;
readonly title: string;
readonly description: string;
readonly instrument: string;
readonly periodStart: string;
readonly periodEnd: string;
readonly frameCount: number;
readonly free: boolean;
}
/**
* Build ONE {@link CatalogListing} in the CANONICAL key order (`id`, `title`, `description`, `instrument`,
* `period` = `{ start, end }`, `frameCount`, `free`). The single constructor BOTH transports call — so
* `JSON.stringify` of a local row and a remote-decoded row for the same subject produce identical bytes.
*/
export declare function toCatalogListing(f: CatalogListingFields): CatalogListing;
/** The snake_case wire row the managed backend's `GET /catalog` serves (a `HostedCatalogEntry`; the live prod
* contract). `artifact_id` is the addressable handle; `content_hash` is an optional replay pin (not part of
* discovery). A light-side self-hosted mirror MAY instead emit the camelCase protocol `id`. */
export interface CatalogListingWireRow {
readonly artifact_id?: string;
readonly id?: string;
readonly title?: string;
readonly description?: string;
readonly instrument?: string;
readonly period?: {
readonly start?: string;
readonly end?: string;
};
readonly frame_count?: number;
readonly frameCount?: number;
readonly free?: boolean;
readonly content_hash?: string;
}
/** Encode a canonical {@link CatalogListing} back to the deployed snake_case wire row — the inverse of
* {@link decodeCatalogListingWire}. Used by the light contract server so its `GET /catalog` speaks the SAME
* dialect as live prod (never the old bare-camelCase mock that hid the divergence, kestrel-y9v1). */
export declare function toCatalogListingWire(l: CatalogListing): Record<string, unknown>;
/**
* The DISCOVERY metadata for each offline-sample catalog subject (kestrel-dnxq), keyed by entry id — the ONE
* source of the human-legible menu fields ({@link CatalogListing} minus `id`). Authored here (a light,
* engine-free module) rather than on the hashed djm.2 entry, because these are DISCOVERY hints, not
* reproducibility pins. Shared by BOTH the LOCAL loader (`catalogListing()`) and the light contract server's
* `GET /catalog`, so a local row and a mock-served-then-decoded row for the same subject are byte-identical.
*
* All three subjects are a single generic QQQ intraday session (`2026-03-02`) presenting the two Backtest Wake
* frames (`RECIPE.wakes.length === 2`, pinned by tests/catalog.session.test.ts); the regime and fill model
* differ. Generic instrument only (no leak).
*/
/**
* The number of decision Wake frames each offline sample presents. A LITERAL here because this module is
* LIGHT (it must not import the engine-bearing `RECIPE`); its honesty is guarded by
* tests/sdk.catalog-equal-projection.test.ts, which asserts it equals `RECIPE_WAKE_COUNT` (the real
* `RECIPE.wakes.length` re-exported from the heavy `session-catalog.ts`). Change one → the guard reds.
*/
export declare const WAKE_FRAMES = 2;
export declare const LISTING_META: Readonly<Record<string, Omit<CatalogListing, "id">>>;
/**
* Build the canonical {@link CatalogListing}[] for a set of catalog ids, in order, from {@link LISTING_META}.
* Fail-closed: an id with no metadata throws {@link CatalogListingDecodeError} (never a silently blank row).
* The shared constructor for the LOCAL projection AND the light contract server's served listing.
*/
export declare function buildCatalogListing(ids: readonly string[]): readonly CatalogListing[];
/**
* Decode a `GET /catalog` body into the canonical {@link CatalogListing}[]. Accepts BOTH observed encodings:
* the deployed `{ entries: [...], version }` wrapper AND a bare array (a self-hosted mirror). Maps each
* snake_case row to the canonical camelCase shape via {@link toCatalogListing} (so the bytes match the LOCAL
* projection exactly). Fail-closed: a body that is neither shape, or a row with no addressable handle
* (`artifact_id`/`id`), throws {@link CatalogListingDecodeError} — never a silent un-openable entry.
*/
export declare function decodeCatalogListingWire(body: unknown): readonly CatalogListing[];
/**
* Extract the optional ex-ante {@link CatalogPricing} block from a `GET /catalog` body (kestrel-adge).
* ABSENT (a bare array, or a `{ entries }` wrapper with no `pricing`) ⇒ `undefined`: today's bare listing,
* fully backward-compatible. When present, the producer's pricing object is surfaced VERBATIM — the whole
* object is passed through (its `skus[].terms`, `settlement_methods[].params`, and any producer field are
* carried unchanged, never rebuilt or dropped), so the platform is a CONSUMER of the OSS contract with no
* divergence. A malformed `pricing` (present but missing a required scalar/array field) is DROPPED, not
* fabricated and not thrown: pricing is an additive discovery HINT, so a bad block must never break the
* listing itself (which stays fail-closed on unaddressable rows via {@link decodeCatalogListingWire}).
*/
export declare function decodeCatalogPricing(body: unknown): CatalogPricing | undefined;
/**
* Decode a `GET /catalog` body into the canonical {@link CatalogPage} (kestrel-adge) — the wrapped
* `{ entries, pricing? }` shape the SDK's `catalog()` returns. The `entries` decode is the SAME fail-closed
* {@link decodeCatalogListingWire} (byte-identical listing rows across transports; throws on an unaddressable
* row); `pricing` rides via {@link decodeCatalogPricing} when the body carries it. A body with no `pricing`
* yields a page whose `entries` are today's bare listing — backward-compatible by construction.
*/
export declare function decodeCatalogPage(body: unknown): CatalogPage;
//# sourceMappingURL=catalog-listing.d.ts.map