UNPKG

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.

59 lines (58 loc) 4.34 kB
/** * # validate — the ONE light arm-time SEMANTIC validator (kestrel-h0ax) * * A LIGHT, session-free composition of the two halves of arm-time SEMANTIC validation for a single * fresh authored document: * * 1. **The six doctrine invariants** (ADR-0004 / ADR-0005) — re-exported from {@link "../lang/validate.ts"}. * They fire at PARSE time already (and at builder construction / print), so a parsed module has * already been checked against them; this module re-exports the `refuse*` guards so a consumer that * authors a document some other way (or wants the invariant vocabulary) reaches them from ONE place. * 2. **The unknown-series case-variant refusal** (kestrel-mte / OSS-ADR-0045) — {@link validateArmSemantics} * over {@link collectUnknownSeriesRefusals}. This is the ONE net-new check parse alone misses: a * trigger naming a CASE-VARIANT of a registered market fact (`HOD` for `hod`) parses clean but * resolves UNKNOWN forever, so the plan arms then never fires (the platform mei bug — parse-only * `/validate` answered `{valid:true}`, `/sim` then `422`'d). * * ## Why a NEW `./validate` subpath, not folded into `/lang` (OSS-ADR-0045; kestrel-h0ax Q3) * The validator spans TWO modules — `lang.validate` (the invariants) AND `series.registry` (the market * vocabulary). `lang` is the FOUNDATIONAL layer that `series` itself imports (`series/types` and * `series/registry` both import `lang` types); folding `series/registry` into `/lang` would INVERT the * layering and risk a dependency cycle. So the clean seam is this NEW module composing `/lang` + * `series/registry`, exported at the `"./validate"` package subpath — ONE honest LIGHT import for the * hosted `/validate` route. * * ## Light by contract (asserted, kestrel-h0ax acceptance) * This module's transitive VALUE import graph carries NO lake/feed/fill/session/bus/native dependency — * only the lang grammar core, `series/registry` (whose only value dep is the clean `series/types`), and * type-only protocol shapes (erased). `tests/validate.import-graph.test.ts` fails the build if a new * import drags any of those in. This is what lets the platform run the SAME arm-time semantic gate a * real arm runs WITHOUT constructing the heavy {@link import("../engine/plans.ts").PlanEngine}. * * ## ONE implementation (no third drifting copy) * `PlanEngine.#unknownSeriesRejection` DELEGATES to {@link unknownSeriesRejection} here, so * `SessionCore.arm` and this light path share ONE code path and return the SAME verdict on the same * document. `tests/validate.arm-parity.test.ts` pins that parity (and that a parse-only check would go * RED — the mei regression). */ // ── 1. the six doctrine invariants (ADR-0004 / ADR-0005), from their single home ────────────────── export * from "../lang/validate.js"; // ── 2. the unknown-series case-variant refusal (kestrel-mte / OSS-ADR-0045) ─────────────────────── export { collectSeriesOperands, unknownSeriesRejection, collectUnknownSeriesRefusals, } from "./unknown-series.js"; // ── the shared phonebook the market-vs-org routing dials, re-exported LIGHT (registry.ts direct) ── export { defaultSeriesRegistry } from "../series/registry.js"; import { collectUnknownSeriesRefusals } from "./unknown-series.js"; import { defaultSeriesRegistry } from "../series/registry.js"; /** * The composed LIGHT arm-time SEMANTIC gate over a PARSED module: the coded {@link ArmRefusal}s a real * arm would return (today, the `unknown-series` case-variant refusals — the doctrine invariants 1–6 are * already enforced by the parse/builder that produced `mod`). Reads only the shared phonebook * (`registry`, defaulting to {@link defaultSeriesRegistry}); no session, no fill, no native dep. * * This is THE shared implementation `PlanEngine.#unknownSeriesRejection` delegates to, so the verdict is * byte-identical to arming the same document through `SessionCore.arm` — the hosted `/validate` route * can answer with the SAME refusal `/sim` would raise, closing the parse ≠ arm gap (the mei bug). */ export function validateArmSemantics(mod, registry = defaultSeriesRegistry) { return collectUnknownSeriesRefusals(mod, registry); }