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
JavaScript
/**
* # 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);
}