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.
271 lines • 18 kB
TypeScript
/**
* # series/registry — the one shared phonebook of series (CONTEXT: Registry)
*
* "Register once → visible to every surface." A **series** is anything with a name whose
* value changes over time (CONTEXT: Series); the design intent is that every surface that
* reads a series — trigger operands, panes, grade columns — dials the *same* phonebook to
* learn what a name *is* before it reads what the name *says*. This module owns that
* phonebook: **one registration record per series NAME**, carrying its {@link SeriesKind
* kind} (market | org), its {@link SeriesScope scope path}, and its {@link Windowing window
* semantics}.
*
* ## What v0 wires (cza.1) — the trigger consumer only
* v0 routes exactly **one** of the three consumers through this phonebook: the **trigger
* operand**. `series/provider` dials {@link SeriesRegistry.lookup} for market-vs-org routing
* (the single place that decision is made), so triggers resolve every market fact by name
* through this table rather than a provider-local copy. **Panes and grade columns do not dial
* the phonebook yet** — panes assemble their level set directly from canonical state
* (`src/session`), and the grade surface is charter-only. Those two consumers are routed
* through this registry as they are built (grade: kestrel-a57.8; observability metadata that
* panes/grade will read: kestrel-cza.3); v0 deliberately does not stand up a second market
* vocabulary for them, it leaves them on their current direct reads until that wiring lands.
*
* ## Hybrid registration (owner-decided)
* There are two-and-only-two kinds of series (CONTEXT: Series), and they register two ways:
*
* - **Market facts + detector outputs are PLATFORM-declared, in code, with full typed
* metadata** — canonical market facts (`spot`, `vwap`, the windowed `velocity/move/range`)
* are ambient signal-space facts the platform *knows a priori*, so they are declared here
* as {@link PLATFORM_SERIES} with their unit, their window semantics, and the market
* {@link MarketBinding binding} the provider dials them through, and each carries its per-series
* observability/fidelity **class** ({@link ObservabilityClass}, kestrel-cza.3) — the finest data
* granularity at which the fact is faithfully observable, which grading enforces
* ({@link SeriesRegistry.admitAtFidelity}).
* - **Org facts + regime tags register IMPLICITLY on first authorized write**, source-stamped
* `authored:` and otherwise **taste-free** — the platform cannot know a pod's `pnl`,
* `plan(x).state`, or open-vocabulary regime tags a priori, so they earn a phonebook entry
* the first time an *authorized* write names them ({@link SeriesRegistry.noteOrgWrite}).
* Registration is not curation: replay decides what earns usage (CONTEXT: Registry —
* "Registration is taste-free; replay decides what earns usage").
*
* **v0 status:** {@link SeriesRegistry.noteOrgWrite} is built and unit-tested here, but v0
* does **not** yet call it from the engine's authorized-write sites (the book's fill fold and
* plan-lifecycle stamps). A live session therefore *resolves* its org facts (through the
* injected `OrgFacts` seam in `series/provider`) but does not yet *register* them, so a
* surface that enumerates the phonebook ({@link SeriesRegistry.dump}) sees the platform market
* facts only. Wiring the org half to real writes is the province of kestrel-cza.4 (the
* child-scoped org-fact resolver); the mechanism, its first-write-wins stamp, its
* never-over-platform guard, and its empty-path fail-closed are proven now so cza.4 only has
* to call it.
*
* ## Fail closed (RUNTIME §2/§8)
* The phonebook never guesses. A NAME with no record resolves to **UNKNOWN** through
* {@link SeriesRegistry.classify} — the same de-arm-with-a-logged-reason path every
* unresolvable series takes; never a silent default. (Registration is a *phonebook* fact,
* not a resolution gate: an unregistered org path still delegates to the book and reads
* UNKNOWN there if the book does not know it — see `series/provider`. `classify` answers the
* narrower question "is there a record for this name?", fail-closed.)
*
* ## No SERIES language surface (v1)
* There is no author-facing `SERIES` statement in v1 — the phonebook is populated by the
* platform in code and implicitly by authorized writes, never by a grammar surface. (If
* author-defined computed series are ever needed, that arrives via an ADR, not here.)
*
* ## Determinism (RUNTIME §0/§7)
* The platform table is a frozen constant; implicit registrations are a pure function of the
* ordered authorized writes. {@link SeriesRegistry.dump} is a sorted, deep-copied snapshot,
* so the same writes yield a byte-identical phonebook.
*/
import type { PathSegment, SeriesRef } from "../lang/index.ts";
import { type Unknown } from "./types.ts";
import type { WindowMetric } from "./windows.ts";
/** The two-and-only-two kinds a series can have (CONTEXT: Series). **market** facts are
* ambient signal-space facts, spectator-visible; **org** facts are path-scoped in the pod
* tree and exist only in an acting session. */
export type SeriesKind = "market" | "org";
/** A series' **window semantics**: a `scalar` carries no window (`spot`, `pnl`); a `windowed`
* series is judged at a timescale against that window's own trailing baseline (`velocity(1m)`,
* CONTEXT: Window). Windowed *org* facts are not a v1 concept, so every org record is scalar. */
export type Windowing = "scalar" | "windowed";
/** A series' **scope path** — where it lives. Market facts are ambient in **signal** space
* (one causal coordinate per signal instrument). Org facts are **org**-scoped: the lexical
* path in the pod tree the write named (`pnl`, `["fills","avg_px"]`, `["plan","state"]`). */
export type SeriesScope = {
readonly space: "signal";
} | {
readonly space: "org";
readonly path: readonly string[];
};
/** The physical unit a platform-declared series carries (typed metadata). Org records are
* taste-free and omit it. Kept small; extend as real facts need it, never stubbed ahead. */
export type SeriesUnit = "price" | "price_delta" | "fraction";
/**
* The per-series **observability / fidelity class** (kestrel-cza.3): the finest
* data granularity at which the series is *faithfully observable*. A series judged on coarser data
* than its class cannot be graded honestly — you cannot see a tick-sensitive series on minute bars —
* so grading enforces it ({@link SeriesRegistry.admitAtFidelity}).
*
* The four rungs run **finest → coarsest** ({@link OBSERVABILITY_ORDER}):
* - `tick` — the value moves trade-by-trade; a faithful read needs every tick (`spot`, `vwap`, and
* every intra-window rate/magnitude metric `velocity`/`move`/`range`, whose sub-minute windows a
* coarser bar cannot reconstruct).
* - `second` — faithful at 1-second resolution (no platform market fact occupies this rung today; it
* exists for second-resolution grading data and future/second-class facts).
* - `minute` — a per-bar summary faithfully captured by minute bars (`hod`/`lod`, `or_high`/`or_low`:
* a running or opening-range extreme that a minute bar's high/low preserves).
* - `session` — one fact per session, faithful with only session-level data (`prior_close`, the prior
* settle).
*/
export type ObservabilityClass = "tick" | "second" | "minute" | "session";
/** The observability lattice, ordered **finest → coarsest**. A series' {@link ObservabilityClass}
* names the finest granularity at which it is faithful; a *grading fidelity* names the finest
* granularity the grader's data (lake / feed) actually provides. Both index into this order. */
export declare const OBSERVABILITY_ORDER: readonly ["tick", "second", "minute", "session"];
/** A **grading fidelity**: the finest data granularity a Session's grading data provides (the lake's
* bar resolution / the feed's cadence). Same lattice as {@link ObservabilityClass} — a session grades
* at one of these rungs, and may only read series that are that-rung-or-COARSER. Distinct from the
* fill-realism `Fidelity` (`modeled | realized`, CONTEXT: Fidelity) — this is data granularity, that
* is fill realism; the two axes are orthogonal. */
export type GradingFidelity = ObservabilityClass;
/**
* True iff a series of class `cls` requires **finer** data than a `fidelity`-graded session provides
* (its rung is strictly finer). Such a series cannot be graded honestly at that fidelity — the
* The lesson: a minute-lake session cannot faithfully see a tick-observable trigger.
*
* **Fail-closed on an off-lattice rung.** If either `cls` or `fidelity` is not a recognized rung
* (e.g. an unrecognized fidelity string that slipped past the type via `as`/JSON), this returns
* `true` — an untrusted rung is treated as "cannot confirm observable", so the caller refuses. It
* never lets a bogus rung compare as coarser-than-everything and silently admit (RUNTIME §2/§8).
*/
export declare function isFinerThanFidelity(cls: ObservabilityClass, fidelity: GradingFidelity): boolean;
/**
* The verdict of the grading-fidelity enforcement seam ({@link SeriesRegistry.admitAtFidelity}):
* either the series is **admitted** (observable at the session's grading fidelity) or **refused**
* with a logged, human-readable `reason` (fail-closed — the caller de-arms the referencing
* statement, RUNTIME §2/§8; it never silently grades on data that cannot see the series).
*/
export type SeriesAdmission = {
readonly admit: true;
} | {
readonly admit: false;
readonly reason: string;
};
/** How the provider dials a **market** fact from canonical state — the resolution binding the
* platform declares alongside the name (so `series/provider` holds only the mechanics of
* reading state, not a second copy of the market-fact vocabulary). */
export type MarketBinding = {
readonly via: "scalar";
readonly scalar: MarketScalar;
} | {
readonly via: "window";
readonly metric: WindowMetric;
readonly pct: boolean;
};
/** The canonical scalar market-fact names (single-segment, no window). */
export type MarketScalar = "spot" | "vwap" | "hod" | "lod" | "prior_close" | "or_high" | "or_low";
/** Where a registration came from — a source-stamp. Platform declarations are `platform`;
* an implicit org registration stamps the authorizing writer (`authored:`). */
export type RegistrationSource = {
readonly kind: "platform";
} | {
readonly kind: "authored";
readonly by: string;
};
/**
* One phonebook entry — **one record per series NAME**. The name is the single-segment head
* used as the key (`spot`, `velocity_pct`, `pnl`, `plan`); path shape beyond the head lives in
* {@link scope}. Market records carry a {@link MarketBinding} + unit; org records are taste-free
* (kind + scope + source only). Market records also carry the {@link observabilityClass} (cza.3).
*/
export interface SeriesRegistration {
readonly name: string;
readonly kind: SeriesKind;
readonly scope: SeriesScope;
readonly windowing: Windowing;
readonly source: RegistrationSource;
/** Typed unit — present on platform (market) declarations; absent on taste-free org records. */
readonly unit?: SeriesUnit;
/** Present only on `kind === "market"`: how the provider resolves the fact. */
readonly market?: MarketBinding;
/** The finest data granularity at which this fact is faithfully observable (kestrel-cza.3).
* Present on every platform (market) declaration; absent on taste-free org records — an org fact
* is the session's own bookkeeping, not a lake read, so lake granularity does not bound it. */
readonly observabilityClass?: ObservabilityClass;
}
/**
* The canonical market-fact vocabulary the platform declares a priori (RUNTIME §2): the seven
* scalar levels + the three window metrics in their `$/window` and `%/window` framings. This is
* the single source of the market-vs-org routing every surface reads through.
*
* (Detector outputs are the other half of the platform declaration set, per the cza epic; they
* join this table when they become *named* series — today they resolve as structural-event
* provider hooks, not named-series reads, so there is no name to register yet.)
*/
export declare const PLATFORM_SERIES: readonly SeriesRegistration[];
/**
* The one shared phonebook of series. Seeded with the frozen platform market-fact declarations;
* grows by implicit org registration on first authorized write. Read by every surface through
* {@link lookup} / {@link classify}.
*/
export declare class SeriesRegistry {
private readonly records;
constructor(platform?: readonly SeriesRegistration[]);
/** The registration for a series NAME (the head-segment name), or undefined when the name is
* not in the phonebook. The provider uses this for market-vs-org routing.
*
* **EXACT-MATCH ONLY** (OSS-ADR-0045). Series names are case-SENSITIVE and the platform market
* facts are lowercase: `lookup("HOD")` MISSES, exactly like `lookup("zzz")`. A case-variant of a
* known market fact is not silently forgiven — the arm-time integrity guard
* ({@link import("../engine/plans.ts").PlanEngine.armDocument}) surfaces it as a coded
* `unknown-series` REFUSAL with a did-you-mean repair, and the author-lint flags it at author
* time. This reverts the 0.4.5 case-insensitive forgiveness (kestrel-7tbi): forgiving a case-clash
* at resolution armed a plan on a name the author never registered and hid a de-arm the author
* could not see — a silent default the platform forbids (AGENTS.md; RUNTIME §8). The phonebook
* never guesses: it answers ONLY for the exact registered name. */
lookup(name: string): SeriesRegistration | undefined;
/**
* Classify a `SeriesRef` → its registration record, or **UNKNOWN** when its head name has no
* phonebook entry (fail-closed — never a silent default). This answers "is there a record for
* this name?"; it is NOT a resolution gate (an unregistered org path still delegates to the
* book — see `series/provider`), it is the phonebook read a surface uses to learn a series'
* kind/scope/window semantics before rendering or grading it.
*/
classify(ref: SeriesRef): SeriesRegistration | Unknown;
/**
* The **grading-fidelity enforcement seam** (kestrel-cza.3). Given the Session's
* grading fidelity (the finest granularity its grading data provides), decide whether a series may
* be graded — and REFUSE, fail-closed with a logged reason, any series **finer** than that fidelity.
* You cannot grade a tick-sensitive series honestly on minute bars: a plan judged on minute-lake
* data may only use minute-observable (or coarser) series.
*
* - **Unknown series** (no phonebook record) ⇒ refuse — an unresolvable series is never graded on a
* guess (fail-closed, mirrors {@link classify}).
* - **Org facts** ⇒ admit — an org fact (`pnl`, `plan.state`) is the acting session's own
* bookkeeping, computed in-session and not read from the market-data lake, so lake granularity
* does not bound its observability.
* - **Market facts** ⇒ admit iff the fact's {@link ObservabilityClass} is `fidelity`-or-COARSER;
* a finer class is refused. A market record missing a class also refuses (fail-closed — an
* unclassified fact's observability cannot be confirmed).
*
* A `true` verdict never fires anything on its own; a `false` verdict carries the de-arm reason the
* caller logs. Pure — no clock, no RNG.
*/
admitAtFidelity(ref: SeriesRef, fidelity: GradingFidelity): SeriesAdmission;
/**
* Record an **org** fact's phonebook entry on first authorized write (the implicit half of
* hybrid registration). Idempotent by NAME — first write wins the source-stamp; later writes
* to the same name (or a sibling path sharing the head) do not re-stamp. Taste-free: it
* records the name/scope/source, it does not judge whether the write *should* exist (replay
* culls). Returns the resulting record. Never registers over a platform (market) name.
*
* **Head-granularity caveat (v0, deliberate).** The record is keyed by the **head** segment
* only (matching {@link classify}, which also keys on the head), so genuinely distinct sibling
* series that share a head — `plan(x).state` vs `plan(x).rungs`, `fills.avg_px` vs
* `fills.count` — collapse to ONE record, and the retained {@link SeriesScope scope path} is
* whichever sibling wrote first (a deterministic first-write artifact, not a claim that the
* others don't exist). Enumerating the phonebook in v0 therefore cannot distinguish collapsed
* siblings. This matches the trigger routing, which keys market-vs-org on the head; per-path
* org records (a record per full scope path, with a path-aware `classify`) are the province of
* kestrel-cza.4, the child-scoped org-fact resolver.
*/
noteOrgWrite(segments: readonly PathSegment[], by: string): SeriesRegistration | Unknown;
/** A deterministic, serializable snapshot of the whole phonebook — records sorted by name,
* deep-copied — for the determinism certification and for surfaces that enumerate the
* phonebook. */
dump(): readonly SeriesRegistration[];
}
/** The process-wide default phonebook, seeded with the platform market facts. The market side is
* static and taste-free, so a shared default is deterministic; a caller that needs an isolated
* phonebook (e.g. to observe implicit org registrations per session) constructs its own. */
export declare const defaultSeriesRegistry: SeriesRegistry;
//# sourceMappingURL=registry.d.ts.map