@mostlyrightmd/core 1.1.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/discovery/index.cjs +44 -7
- package/dist/discovery/index.cjs.map +1 -1
- package/dist/discovery/index.d.cts +34 -6
- package/dist/discovery/index.d.ts +34 -6
- package/dist/discovery/index.mjs +44 -7
- package/dist/discovery/index.mjs.map +1 -1
- package/dist/index.cjs +39 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.global.js +37 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.mjs +37 -1
- package/dist/index.mjs.map +1 -1
- package/dist/internal/cache/index.browser.cjs +77 -4
- package/dist/internal/cache/index.browser.cjs.map +1 -1
- package/dist/internal/cache/index.browser.d.cts +8 -2
- package/dist/internal/cache/index.browser.d.ts +8 -2
- package/dist/internal/cache/index.browser.mjs +10 -5
- package/dist/internal/cache/index.browser.mjs.map +1 -1
- package/dist/internal/cache/index.cjs +79 -2
- package/dist/internal/cache/index.cjs.map +1 -1
- package/dist/internal/cache/index.d.cts +12 -6
- package/dist/internal/cache/index.d.ts +12 -6
- package/dist/internal/cache/index.mjs +12 -3
- package/dist/internal/cache/index.mjs.map +1 -1
- package/dist/internal/{chunk-PKJXHY27.mjs → chunk-IPC4XUYW.mjs} +70 -1
- package/dist/internal/chunk-IPC4XUYW.mjs.map +1 -0
- package/dist/internal/{keys-B7C8C88N.d.cts → versionedCacheStore-DyHDqFIC.d.cts} +23 -1
- package/dist/internal/{keys-B7C8C88N.d.ts → versionedCacheStore-DyHDqFIC.d.ts} +23 -1
- package/dist/preprocessing/index.cjs +150 -0
- package/dist/preprocessing/index.cjs.map +1 -0
- package/dist/preprocessing/index.d.cts +111 -0
- package/dist/preprocessing/index.d.ts +111 -0
- package/dist/preprocessing/index.mjs +121 -0
- package/dist/preprocessing/index.mjs.map +1 -0
- package/dist/temporal/index.cjs.map +1 -1
- package/dist/temporal/index.mjs.map +1 -1
- package/dist/validator.cjs.map +1 -1
- package/dist/validator.mjs.map +1 -1
- package/package.json +25 -2
- package/dist/internal/chunk-PKJXHY27.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/temporal/index.ts","../../src/temporal/timepoint.ts","../../src/exceptions/index.ts","../../src/temporal/knowledge-view.ts","../../src/temporal/leakage.ts"],"sourcesContent":["// Barrel for @mostlyrightmd/core/temporal — TS-W3 Plan 04.\n//\n// Public API: TimePoint, KnowledgeView, LeakageDetector, assertNoLeakage.\n\nexport { TimePoint } from \"./timepoint.js\";\nexport { KnowledgeView } from \"./knowledge-view.js\";\nexport { LeakageDetector, assertNoLeakage } from \"./leakage.js\";\n","// UTC-aware timestamp wrapper for mostlyright.core (TS-W3 Plan 04 Task 1).\n//\n// Mirrors `packages/core/src/mostlyright/core/temporal/timepoint.py`.\n// Every timestamp in mostlyright.core is UTC-aware. TimePoint rejects:\n// - naive ISO strings (no Z, no +HH:MM, no -HH:MM offset)\n// - date-only ISO strings (e.g. \"2026-05-21\" — no time component)\n// - date-only ISO strings with a timezone suffix (e.g. \"2026-05-21Z\" or\n// \"2026-05-21+00:00\") — Python `_from_iso_string` rejects these by\n// requiring a `T` (or space) separator BEFORE checking the tz suffix.\n// - NaN / Infinity / -Infinity (via Date(NaN), Date(Infinity), etc.)\n// - empty / whitespace-only strings\n// - non-Date, non-string inputs (TypeError; belt-and-suspenders runtime guard)\n//\n// Note on Date inputs: a JS `Date` is just epoch ms — no timezone metadata.\n// \"Naive\" only applies to STRING inputs, where we can inspect the source\n// text for a timezone indicator. For Date inputs we only reject NaN/Infinity.\n//\n// Iter-11 C13: TimePoint preserves MICROSECOND precision when constructed\n// from a string with 4-6 digit fractional seconds. Internally we still\n// carry a `Date` (epoch-ms, used for `toUTCDate()` and `asZone()` since\n// JS Date has no µs precision), but we ALSO carry a `bigint` epoch-µs\n// field used for ALL equality / before / after comparisons. Without this,\n// `Date.parse(\"2025-01-02T12:00:00.123456Z\")` collapses to the same\n// epoch-ms as `.123789Z`, and `assertNoLeakage()` would silently miss a\n// row \"known\" 333µs after `asOf` — a temporal-safety violation.\n//\n// BigInt is fully supported in all target runtimes (Node 16+, modern\n// browsers, MV3 service workers per ES2020). The Date class is left\n// ms-only on purpose — that's fine for display/timezone operations, and\n// `toUTCDate()` deliberately returns ms precision for back-compat. The\n// µs precision is for comparison/equality only.\n\nconst ISO_DATE_ONLY = /^\\d{4}-\\d{2}-\\d{2}$/;\n// ISO 8601 requires a `T` (or RFC 3339 space) separator between the date\n// and time components. Without one, the string is a bare date — even if a\n// timezone suffix is appended (`\"2026-05-21Z\"`). Mirrors the Python guard\n// in `_from_iso_string` that checks `\"T\" not in s and \" \" not in s`.\nconst DATETIME_SEPARATOR = /[T ]/;\nconst TZ_SUFFIX = /(?:Z|[+-]\\d{2}:?\\d{2})$/;\n// Calendar-validity check (iter-3 C8): extract the YYYY-MM-DD prefix so we\n// can verify that the parsed UTC date matches the literal date the caller\n// supplied. `Date.parse(\"2025-02-30T00:00:00Z\")` silently normalizes to\n// `2025-03-02T00:00:00.000Z`, while Python `datetime.fromisoformat(...)`\n// raises ValueError. Mirror the Python contract by rejecting any string\n// whose calendar fields don't survive the round-trip.\nconst ISO_DATE_PREFIX = /^(\\d{4})-(\\d{2})-(\\d{2})/;\n// Iter-11 C13: extract the fractional-seconds digits between the seconds\n// field and the tz suffix. Captures 1-9 digits; Python `fromisoformat`\n// truncates beyond 6 (microseconds), so we do the same. Anchored at a\n// `.` after the `HH:MM:SS` segment and bounded by the start of the tz\n// suffix (`Z`, `+`, or `-`).\nconst FRACTIONAL_SECONDS = /[T ]\\d{2}:\\d{2}:\\d{2}\\.(\\d+)(?:Z|[+-])/;\n\n/**\n * UTC-aware timestamp wrapper.\n *\n * Equivalent to Python's `mostlyright.core.TimePoint`. Constructed from either\n * a `Date` (rejects NaN/Infinity) or a tz-aware ISO 8601 string (rejects\n * naive / date-only inputs). Internally stored as an epoch-ms Date (for\n * display/timezone ops) PLUS an epoch-µs `bigint` (used for comparisons).\n *\n * Immutable: the underlying Date is hidden behind a private `#utc` field\n * and `toUTCDate()` returns a defensive copy so callers cannot mutate the\n * wrapped instant. The `#epochMicros` field carries full µs precision\n * captured from string inputs with 4-6 digit fractional seconds.\n */\nexport class TimePoint {\n // Private fields — naive ISO strings cannot leak back through any accessor.\n readonly #utc: Date;\n // Iter-11 C13: full µs precision (epoch microseconds). For Date inputs\n // and string inputs with ≤3 fractional digits this is just\n // `#utc.getTime() * 1000n`. For string inputs with 4-6 fractional digits\n // we capture the true µs value from the source string.\n readonly #epochMicros: bigint;\n\n constructor(value: Date | string) {\n if (value instanceof Date) {\n const t = value.getTime();\n if (!Number.isFinite(t)) {\n // RangeError for NaN/Infinity — see \"date-only\" / \"naive\" peers below.\n throw new RangeError(\"TimePoint does not accept NaN/Infinity Date\");\n }\n this.#utc = new Date(t);\n // Date inputs only carry ms precision — derive µs trivially.\n this.#epochMicros = BigInt(t) * 1000n;\n return;\n }\n if (typeof value !== \"string\") {\n throw new TypeError(`TimePoint accepts Date or ISO 8601 string; got ${typeof value}`);\n }\n const trimmed = value.trim();\n if (trimmed.length === 0) {\n throw new RangeError(\"TimePoint requires non-empty ISO 8601 string\");\n }\n // Date-only check: a bare \"YYYY-MM-DD\" has no time component.\n if (ISO_DATE_ONLY.test(trimmed)) {\n throw new RangeError(\n `TimePoint requires datetime, not date-only (got ${JSON.stringify(\n value,\n )}). Use an ISO 8601 datetime with a timezone, e.g. '2026-05-21T14:30:00Z'.`,\n );\n }\n // Separator check (MUST run BEFORE the tz-suffix check): a date-only\n // payload like \"2026-05-21Z\" or \"2026-05-21+00:00\" has no `T`/space\n // separator and is still a bare date — Date.parse silently normalizes\n // it to midnight UTC, but Python `_from_iso_string` rejects it. Reject\n // here so the constructor matches the Python contract exactly.\n if (!DATETIME_SEPARATOR.test(trimmed)) {\n throw new RangeError(\n `TimePoint requires datetime, not date-only (got ${JSON.stringify(\n value,\n )}). ISO 8601 requires a 'T' or space separator between the date and time, e.g. '2026-05-21T14:30:00Z'.`,\n );\n }\n // Naive check: must end in Z, +HH:MM, +HHMM, -HH:MM, or -HHMM.\n if (!TZ_SUFFIX.test(trimmed)) {\n throw new RangeError(\n `TimePoint requires tz-aware timestamp; got naive ISO string (${JSON.stringify(\n value,\n )}). Include a timezone offset (e.g. 'Z' or '+00:00').`,\n );\n }\n const parsed = Date.parse(trimmed);\n if (!Number.isFinite(parsed)) {\n throw new RangeError(`TimePoint could not parse ISO 8601 string ${JSON.stringify(value)}`);\n }\n // Calendar-validity check (iter-3 C8): `Date.parse` is forgiving — it\n // silently normalizes impossible dates (e.g. \"2025-02-30T...\" becomes\n // \"2025-03-02T...\", \"2025-13-01T...\" becomes \"2026-01-01T...\"). Python\n // `datetime.fromisoformat` raises ValueError for the same inputs. To\n // match the Python contract, extract the YYYY-MM-DD prefix from the\n // ORIGINAL trimmed input and require that the parsed Date's UTC\n // year/month/day match exactly. Any mismatch means Date.parse rolled\n // the date over — reject loudly. The parsed date's UTC fields are the\n // right basis for comparison because non-UTC tz-suffixes (e.g.\n // \"...T23:00:00-05:00\") legitimately shift the wall-clock date forward\n // when converted to UTC, but only by ±1 day; an off-by-one shift due\n // to a legitimate tz offset will still hit this check, so we ALSO\n // accept a match against the local fields the source string asserts.\n //\n // Strategy: compute what the source-side year/month/day would be after\n // applying the declared UTC offset, then compare those derived fields\n // against the literal YYYY-MM-DD in the string. If the source string\n // was an impossible calendar date, Date.parse's silent normalization\n // will have shifted the underlying ms beyond what the declared offset\n // alone could account for, so the derived fields won't match.\n const dateMatch = ISO_DATE_PREFIX.exec(trimmed);\n if (dateMatch !== null) {\n const litYear = Number(dateMatch[1]);\n const litMonth = Number(dateMatch[2]);\n const litDay = Number(dateMatch[3]);\n // Derive the source-side date by undoing the declared tz offset.\n // The trailing tz suffix is one of: \"Z\", \"+HH:MM\", \"-HH:MM\", \"+HHMM\",\n // \"-HHMM\". For \"Z\" the offset is 0. For the signed forms, the offset\n // is the source's distance ahead/behind UTC: a \"-05:00\" tz on\n // \"10:00:00\" means UTC is 15:00:00, so to recover the source's\n // wall-clock year/month/day we ADD the offset back to the UTC ms\n // before extracting Y/M/D. Equivalently: build a Date at the same\n // ms, then read its UTC fields after offsetting.\n let offsetMin = 0;\n const tzMatch = /(Z|[+-]\\d{2}:?\\d{2})$/.exec(trimmed);\n // Under `noUncheckedIndexedAccess: true`, `tzMatch[1]` is typed\n // `string | undefined` even after the `null` guard. Capture it once\n // and narrow with an explicit undefined check (unreachable when\n // tzMatch is non-null — the regex always populates group 1 — but\n // the type checker can't prove that on its own).\n const tz = tzMatch === null ? undefined : tzMatch[1];\n if (tz !== undefined && tz !== \"Z\") {\n // tz is like \"+05:30\", \"-0500\", \"+0000\".\n const sign = tz.startsWith(\"-\") ? -1 : 1;\n const body = tz.slice(1).replace(\":\", \"\");\n const hh = Number(body.slice(0, 2));\n const mm = Number(body.slice(2, 4));\n offsetMin = sign * (hh * 60 + mm);\n }\n // The source's wall-clock instant in ms-since-epoch: take the UTC ms\n // and add the offset (positive offset means source is ahead of UTC).\n const sourceMs = parsed + offsetMin * 60_000;\n const sourceDate = new Date(sourceMs);\n const derivedYear = sourceDate.getUTCFullYear();\n const derivedMonth = sourceDate.getUTCMonth() + 1;\n const derivedDay = sourceDate.getUTCDate();\n if (derivedYear !== litYear || derivedMonth !== litMonth || derivedDay !== litDay) {\n throw new RangeError(\n `TimePoint rejects impossible calendar date in ${JSON.stringify(\n value,\n )}: literal date ${dateMatch[0]} does not survive round-trip (parser normalized to ${derivedYear}-${String(derivedMonth).padStart(2, \"0\")}-${String(derivedDay).padStart(2, \"0\")}). Python datetime.fromisoformat raises ValueError on this input.`,\n );\n }\n }\n this.#utc = new Date(parsed);\n // Iter-11 C13: capture full µs precision from the source string.\n //\n // `Date.parse` truncates fractional seconds to ms (3 digits). To\n // recover µs precision when the source carries 4-6 digits, we scan\n // the trimmed string for the `.<digits>` segment between `HH:MM:SS`\n // and the tz suffix, then convert the captured digits to a µs\n // remainder added on top of the seconds-aligned epoch-µs base.\n //\n // The \"base\" is the whole-seconds portion of the parsed UTC ms:\n // `floor(parsed / 1000) * 1_000_000n`. We then add the fractional\n // part as µs. This avoids relying on `Date.parse`'s lossy ms-only\n // fractional handling entirely. Python `datetime.fromisoformat`\n // accepts arbitrary fractional precision but truncates to 6 digits\n // (µs) — we mirror that: take only the first 6 digits, ignore the\n // rest. Anything past digit 6 is below µs precision and must NOT\n // round up (Python truncates; we match).\n const fracMatch = FRACTIONAL_SECONDS.exec(trimmed);\n if (fracMatch !== null) {\n const digits = fracMatch[1] ?? \"\";\n // Pad to 6 (so \".1\" → \"100000\" µs) and truncate to 6 (so\n // \".1234567\" → \"123456\" µs; the 7th digit is sub-µs and dropped,\n // matching Python's behavior).\n const microDigits = `${digits}000000`.slice(0, 6);\n const fractionalMicros = BigInt(microDigits);\n // Whole-second floor of the parsed UTC instant, in µs. Use BigInt\n // arithmetic throughout so we don't lose precision crossing the\n // ±2^53 ms threshold (parsed is in ms; Math.floor on a regular\n // number is still safe in the ms range, but the conversion to µs\n // — multiply by 1e6 — would NOT be safe as a regular number for\n // any realistic timestamp, so do the math in BigInt).\n const parsedMs = BigInt(parsed);\n const wholeSecondsMs = (parsedMs / 1000n) * 1000n;\n const wholeSecondsMicros = wholeSecondsMs * 1000n;\n this.#epochMicros = wholeSecondsMicros + fractionalMicros;\n } else {\n // No fractional component in the source string — the ms-precision\n // `parsed` value is already the full truth.\n this.#epochMicros = BigInt(parsed) * 1000n;\n }\n }\n\n /** Return a defensive copy of the underlying UTC Date. */\n toUTCDate(): Date {\n return new Date(this.#utc.getTime());\n }\n\n /**\n * Return the epoch microseconds as a `bigint`.\n *\n * Iter-11 C13: this is the comparison-safe accessor. JS `Date` only\n * carries ms precision, so callers comparing `toUTCDate().getTime()`\n * across two TimePoints constructed from `.123456Z` and `.123789Z`\n * would see them as equal — they're not. Use `equals`/`before`/`after`\n * (or this accessor directly) to compare with full µs precision.\n */\n toEpochMicros(): bigint {\n return this.#epochMicros;\n }\n\n /** Return the canonical ISO 8601 UTC string (always ends in 'Z'). */\n toISOString(): string {\n return this.#utc.toISOString();\n }\n\n /**\n * Return the Python-compatible ISO 8601 UTC string.\n *\n * Where {@link toISOString} emits the JS-native `\"...Z\"` suffix and\n * forces `.000` millisecond padding, this method matches Python's\n * `datetime.isoformat()` output for a UTC-tz-aware datetime:\n *\n * - tz suffix is `+00:00`, never `Z`\n * - subsecond portion is omitted when zero (`\"...T12:00:00+00:00\"`)\n * - microsecond portion appears as 6 digits when non-zero\n * (`\"...T12:00:00.123456+00:00\"`); built from the µs-precision\n * `#epochMicros` field so 4-6 digit fractional inputs round-trip\n * exactly (iter-11 C13 fix).\n *\n * Used by error payloads (LeakageError.toDict, sample violations) so\n * MCP clients comparing the on-wire string across the Python and TS\n * SDKs see byte-equivalent values. Iter-1 H1 fix; iter-11 C13 extends\n * to full µs precision.\n */\n toPythonIso(): string {\n const year = this.#utc.getUTCFullYear();\n const month = String(this.#utc.getUTCMonth() + 1).padStart(2, \"0\");\n const day = String(this.#utc.getUTCDate()).padStart(2, \"0\");\n const hour = String(this.#utc.getUTCHours()).padStart(2, \"0\");\n const minute = String(this.#utc.getUTCMinutes()).padStart(2, \"0\");\n const second = String(this.#utc.getUTCSeconds()).padStart(2, \"0\");\n const head = `${year}-${month}-${day}T${hour}:${minute}:${second}`;\n // Iter-11 C13: derive the µs fraction from `#epochMicros`, not from\n // `Date.getUTCMilliseconds()`. The Date is ms-only, so for a source\n // like \".123456Z\" the Date's ms field is 123 — emitting that as\n // \".123000+00:00\" silently drops 456µs. The µs field carries the\n // full truth.\n //\n // `#epochMicros % 1_000_000n` is the µs-within-the-second component\n // for any non-negative epoch. For pre-epoch timestamps (rare in\n // mostlyright — design.md says UTC and ≥ 2018), BigInt's `%` rounds\n // toward zero, which would produce a negative remainder. Python's\n // `%` rounds toward negative infinity (always non-negative for a\n // positive divisor). Normalize by adding the divisor back if the\n // remainder is negative, so we match Python's behavior at the edge.\n const ONE_MILLION = 1_000_000n;\n let microsInSecond = this.#epochMicros % ONE_MILLION;\n if (microsInSecond < 0n) microsInSecond += ONE_MILLION;\n if (microsInSecond === 0n) {\n return `${head}+00:00`;\n }\n const micros = String(microsInSecond).padStart(6, \"0\");\n return `${head}.${micros}+00:00`;\n }\n\n /**\n * Format this instant in an IANA timezone via Intl.DateTimeFormat.\n * Display helper only — canonical storage stays UTC.\n *\n * Uses `en-CA` locale for a YYYY-MM-DD, HH:MM:SS shape that's easy to grep\n * in logs. The exact output format may vary slightly across Node releases;\n * callers writing tests should use loose contains-style assertions.\n */\n asZone(tz: string): string {\n return new Intl.DateTimeFormat(\"en-CA\", {\n timeZone: tz,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n }).format(this.#utc);\n }\n\n equals(other: TimePoint): boolean {\n // Iter-11 C13: compare epoch-µs, NOT epoch-ms. Two TimePoints from\n // `.123456Z` and `.123789Z` have the same ms but different µs —\n // they're distinct instants and must compare unequal so a \"knowledge\n // 333µs after asOf\" row is correctly flagged as leakage.\n return this.#epochMicros === other.#epochMicros;\n }\n\n before(other: TimePoint): boolean {\n return this.#epochMicros < other.#epochMicros;\n }\n\n after(other: TimePoint): boolean {\n return this.#epochMicros > other.#epochMicros;\n }\n\n /** Return a TimePoint for the current UTC instant. */\n static now(): TimePoint {\n return new TimePoint(new Date());\n }\n}\n","// Structured exception hierarchy for the mostlyright TS SDK.\n//\n// Mirrors the Python design in `packages/core/src/mostlyright/core/exceptions.py`\n// and `packages/core/src/mostlyright/_internal/exceptions.py`. Every error\n// subclasses `TradewindsError` and exposes a `toDict()` method that returns a\n// JSON-safe payload suitable for MCP `error.data` / extension messaging.\n//\n// Role-name vocabulary for SourceMismatchError matches Python:\n// \"observations\" / \"forecasts\" / \"settlement\" (long form, NOT the col prefixes)\n\n// ---------------------------------------------------------------------------\n// JSON-safe coercion (mirrors `mostlyright.core._json_safe.to_json_safe`)\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively coerce `value` into a JSON-serializable structure.\n *\n * Coercion rules (mirrors Python's `to_json_safe`):\n * - `null` / `undefined` / `NaN` / `Infinity` / `-Infinity` → `null`\n * - `Date` → ISO 8601 UTC string ending in `Z`\n * - Numeric / boolean / string scalars pass through (non-finite numbers → null)\n * - Arrays + plain objects → recursive (cycles → `{ _cycle: true, value: String(obj) }`)\n * - Dict keys MUST be strings; non-string keys throw `TypeError`.\n * - Anything else (Symbol, function, class instance without `toJSON`) →\n * `{ _repr_only: true, value: String(value) }`.\n */\nexport function toJsonSafe(value: unknown, seen?: WeakSet<object>): unknown {\n const visited = seen ?? new WeakSet<object>();\n\n // null / undefined → null\n if (value === null || value === undefined) {\n return null;\n }\n\n // Booleans pass through.\n if (typeof value === \"boolean\") {\n return value;\n }\n\n // Strings pass through.\n if (typeof value === \"string\") {\n return value;\n }\n\n // Numbers — non-finite (NaN, +/-Infinity) coerce to null.\n if (typeof value === \"number\") {\n return Number.isFinite(value) ? value : null;\n }\n\n // bigint → number when safe, else stringify (JSON can't natively encode bigint).\n if (typeof value === \"bigint\") {\n // Match Python: integers pass through as native numeric. Safe-range only.\n if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {\n return Number(value);\n }\n return { _repr_only: true, value: value.toString() };\n }\n\n // Date → ISO 8601 UTC string (always ending in \"Z\").\n if (value instanceof Date) {\n if (Number.isNaN(value.getTime())) {\n return null;\n }\n return value.toISOString();\n }\n\n // Arrays — recurse, track cycles.\n if (Array.isArray(value)) {\n if (visited.has(value)) {\n return { _cycle: true, value: String(value) };\n }\n visited.add(value);\n try {\n return value.map((item) => toJsonSafe(item, visited));\n } finally {\n visited.delete(value);\n }\n }\n\n // Plain objects — recurse, track cycles, enforce string keys.\n if (typeof value === \"object\") {\n if (visited.has(value as object)) {\n return { _cycle: true, value: String(value) };\n }\n visited.add(value as object);\n try {\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(value as Record<string, unknown>)) {\n if (typeof key !== \"string\") {\n throw new TypeError(`toJsonSafe dict keys must be string; got ${typeof key}`);\n }\n out[key] = toJsonSafe((value as Record<string, unknown>)[key], visited);\n }\n return out;\n } finally {\n visited.delete(value as object);\n }\n }\n\n // Symbols, functions, etc. — repr-only marker.\n return { _repr_only: true, value: String(value) };\n}\n\n// ---------------------------------------------------------------------------\n// Base class\n// ---------------------------------------------------------------------------\n\nexport interface TradewindsErrorOptions {\n errorCode?: string;\n source?: string | null;\n requestId?: string | null;\n}\n\n/**\n * Base class for all mostlyright structured errors.\n *\n * `errorCode` is a stable enum (e.g. \"SOURCE_UNAVAILABLE\") used by callers /\n * agents to branch on without parsing message text. `source` is the source id\n * involved (e.g. \"iem.archive\") when applicable, and `requestId` correlates a\n * JSON-RPC / MCP request id when applicable.\n */\nexport class TradewindsError extends Error {\n /** Subclass override — the stable string enum surfaced via `errorCode`. */\n static defaultErrorCode = \"TRADEWINDS_ERROR\";\n\n readonly errorCode: string;\n readonly source: string | null;\n readonly requestId: string | null;\n\n constructor(message = \"\", options: TradewindsErrorOptions = {}) {\n super(message);\n this.name = new.target.name;\n const ctor = this.constructor as typeof TradewindsError;\n this.errorCode = options.errorCode ?? ctor.defaultErrorCode;\n this.source = options.source ?? null;\n this.requestId = options.requestId ?? null;\n // Restore prototype chain after `Error` (needed for `instanceof` across\n // transpilation / ES5 targets).\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n /**\n * Subclass hook returning the structured attributes for `toDict`.\n * Values are passed through `toJsonSafe` by `toDict()`, so subclasses\n * don't need to coerce values themselves.\n */\n protected payload(): Record<string, unknown> {\n return {\n error_code: this.errorCode,\n message: this.message,\n source: this.source,\n request_id: this.requestId,\n };\n }\n\n /** Return a JSON-safe dict suitable for MCP `error.data`. */\n toDict(): Record<string, unknown> {\n const safe = toJsonSafe(this.payload());\n return safe as Record<string, unknown>;\n }\n}\n\n// ---------------------------------------------------------------------------\n// SourceUnavailableError\n// ---------------------------------------------------------------------------\n\nexport interface SourceUnavailableErrorOptions extends TradewindsErrorOptions {\n httpStatus?: number | null;\n retryable?: boolean;\n retryAfterS?: number | null;\n underlying?: string;\n url?: string | null;\n}\n\nexport class SourceUnavailableError extends TradewindsError {\n static override defaultErrorCode = \"SOURCE_UNAVAILABLE\";\n\n readonly httpStatus: number | null;\n readonly retryable: boolean;\n readonly retryAfterS: number | null;\n readonly underlying: string;\n readonly url: string | null;\n\n constructor(message = \"\", options: SourceUnavailableErrorOptions = {}) {\n super(message, options);\n this.httpStatus = options.httpStatus ?? null;\n this.retryable = options.retryable ?? false;\n this.retryAfterS = options.retryAfterS ?? null;\n this.underlying = options.underlying ?? \"\";\n this.url = options.url ?? null;\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n http_status: this.httpStatus,\n retryable: this.retryable,\n retry_after_s: this.retryAfterS,\n underlying: this.underlying,\n url: this.url,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// SchemaValidationError\n// ---------------------------------------------------------------------------\n\nexport interface SchemaValidationErrorOptions extends TradewindsErrorOptions {\n schemaId: string;\n violations?: Array<Record<string, unknown>>;\n quarantineCount?: number;\n sampleViolations?: Array<Record<string, unknown>>;\n}\n\nexport class SchemaValidationError extends TradewindsError {\n static override defaultErrorCode = \"SCHEMA_VALIDATION_FAILED\";\n\n readonly schemaId: string;\n readonly violations: Array<Record<string, unknown>>;\n readonly quarantineCount: number;\n readonly sampleViolations: Array<Record<string, unknown>>;\n\n constructor(message: string, options: SchemaValidationErrorOptions) {\n super(message, options);\n this.schemaId = options.schemaId;\n this.violations = [...(options.violations ?? [])];\n this.quarantineCount = options.quarantineCount ?? 0;\n this.sampleViolations = [...(options.sampleViolations ?? [])];\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n schema_id: this.schemaId,\n violations: this.violations,\n quarantine_count: this.quarantineCount,\n sample_violations: this.sampleViolations,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// SourceMismatchError\n// ---------------------------------------------------------------------------\n\nexport type SourceMismatchRole = \"observations\" | \"forecasts\" | \"settlement\";\n\nexport interface SourceMismatchErrorOptions extends TradewindsErrorOptions {\n schemaSource: string;\n dataSource: string;\n role?: SourceMismatchRole | null;\n catalogWarning?: string | null;\n}\n\nexport class SourceMismatchError extends TradewindsError {\n static override defaultErrorCode = \"SOURCE_MISMATCH\";\n\n /** Canonical role-name vocabulary (design.md §R). */\n static readonly VALID_ROLES: ReadonlySet<SourceMismatchRole> = new Set([\n \"observations\",\n \"forecasts\",\n \"settlement\",\n ]);\n\n readonly schemaSource: string;\n readonly dataSource: string;\n readonly role: SourceMismatchRole | null;\n readonly catalogWarning: string | null;\n\n constructor(message: string, options: SourceMismatchErrorOptions) {\n super(message, options);\n this.schemaSource = options.schemaSource;\n this.dataSource = options.dataSource;\n this.role = options.role ?? null;\n this.catalogWarning = options.catalogWarning ?? null;\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n schema_source: this.schemaSource,\n data_source: this.dataSource,\n role: this.role,\n catalog_warning: this.catalogWarning,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// LeakageError\n// ---------------------------------------------------------------------------\n\nexport interface LeakageErrorOptions extends TradewindsErrorOptions {\n asOf: string;\n violatingCount: number;\n sampleViolations?: Array<Record<string, unknown>>;\n}\n\nexport class LeakageError extends TradewindsError {\n static override defaultErrorCode = \"LEAKAGE_DETECTED\";\n\n readonly asOf: string;\n readonly violatingCount: number;\n readonly sampleViolations: Array<Record<string, unknown>>;\n\n constructor(message: string, options: LeakageErrorOptions) {\n super(message, options);\n this.asOf = options.asOf;\n this.violatingCount = options.violatingCount;\n this.sampleViolations = [...(options.sampleViolations ?? [])];\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n as_of: this.asOf,\n violating_count: this.violatingCount,\n sample_violations: this.sampleViolations,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// TemporalDriftError\n// ---------------------------------------------------------------------------\n\nexport interface TemporalDriftErrorOptions extends TradewindsErrorOptions {\n schemaId: string;\n assertedRange: [string, string];\n violatingRows: number;\n sampleViolations?: Array<Record<string, unknown>>;\n}\n\nexport class TemporalDriftError extends TradewindsError {\n static override defaultErrorCode = \"TEMPORAL_DRIFT\";\n\n readonly schemaId: string;\n readonly assertedRange: [string, string];\n readonly violatingRows: number;\n readonly sampleViolations: Array<Record<string, unknown>>;\n\n constructor(message: string, options: TemporalDriftErrorOptions) {\n super(message, options);\n this.schemaId = options.schemaId;\n this.assertedRange = [options.assertedRange[0], options.assertedRange[1]];\n this.violatingRows = options.violatingRows;\n this.sampleViolations = [...(options.sampleViolations ?? [])];\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n schema_id: this.schemaId,\n asserted_range: [this.assertedRange[0], this.assertedRange[1]],\n violating_rows: this.violatingRows,\n sample_violations: this.sampleViolations,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// PayloadTooLargeError\n// ---------------------------------------------------------------------------\n\nexport interface PayloadTooLargeErrorOptions extends TradewindsErrorOptions {\n declaredSize: number;\n limit: number;\n acceptedModes?: string[];\n}\n\nexport class PayloadTooLargeError extends TradewindsError {\n static override defaultErrorCode = \"PAYLOAD_TOO_LARGE\";\n\n readonly declaredSize: number;\n readonly limit: number;\n readonly acceptedModes: string[];\n\n constructor(message: string, options: PayloadTooLargeErrorOptions) {\n super(message, options);\n this.declaredSize = options.declaredSize;\n this.limit = options.limit;\n this.acceptedModes = [...(options.acceptedModes ?? [])];\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n declared_size: this.declaredSize,\n limit: this.limit,\n accepted_modes: this.acceptedModes,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// DeferredMarketError (TS-W6 placeholder; mirrors mostlyright.international)\n// ---------------------------------------------------------------------------\n\nexport class DeferredMarketError extends TradewindsError {\n static override defaultErrorCode = \"DEFERRED_MARKET\";\n}\n\n// ---------------------------------------------------------------------------\n// PolymarketEventError (TS-W5 placeholder; mirrors mostlyright.markets.polymarket)\n// ---------------------------------------------------------------------------\n\nexport class PolymarketEventError extends TradewindsError {\n static override defaultErrorCode = \"POLYMARKET_EVENT_INVALID\";\n}\n\n// ---------------------------------------------------------------------------\n// HTTP-layer hierarchy (mirrors mostlyright._internal.exceptions)\n// ---------------------------------------------------------------------------\n\nexport interface TherminalErrorOptions extends TradewindsErrorOptions {\n statusCode?: number | null;\n retryAfter?: number | null;\n}\n\n/**\n * Base HTTP-layer marker. Subclass of `TradewindsError` so callers that\n * catch `TradewindsError` also catch transport errors.\n */\nexport class TherminalError extends TradewindsError {\n static override defaultErrorCode = \"HTTP_ERROR\";\n\n readonly statusCode: number | null;\n\n constructor(message: string, options: TherminalErrorOptions = {}) {\n super(message, options);\n this.statusCode = options.statusCode ?? null;\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n status_code: this.statusCode,\n };\n }\n}\n\nexport class NotFoundError extends TherminalError {\n static override defaultErrorCode = \"HTTP_NOT_FOUND\";\n\n constructor(message = \"Resource not found\", options: TherminalErrorOptions = {}) {\n super(message, { ...options, statusCode: options.statusCode ?? 404 });\n }\n}\n\nexport interface RateLimitErrorOptions extends TherminalErrorOptions {\n retryAfter?: number | null;\n}\n\nexport class RateLimitError extends TherminalError {\n static override defaultErrorCode = \"HTTP_RATE_LIMITED\";\n\n readonly retryAfter: number | null;\n\n constructor(retryAfter: number | null = 1, options: RateLimitErrorOptions = {}) {\n const msg = `Rate limit exceeded. Retry after ${retryAfter ?? \"?\"}s`;\n super(msg, { ...options, statusCode: options.statusCode ?? 429 });\n this.retryAfter = retryAfter;\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n retry_after: this.retryAfter,\n };\n }\n}\n\nexport class ValidationError extends TherminalError {\n static override defaultErrorCode = \"HTTP_BAD_REQUEST\";\n\n constructor(message = \"Invalid request\", options: TherminalErrorOptions = {}) {\n super(message, { ...options, statusCode: options.statusCode ?? 400 });\n }\n}\n\nexport class AuthenticationError extends TherminalError {\n static override defaultErrorCode = \"HTTP_UNAUTHORIZED\";\n\n constructor(message = \"Authentication required\", options: TherminalErrorOptions = {}) {\n super(message, { ...options, statusCode: options.statusCode ?? 401 });\n }\n}\n\nexport class ForbiddenError extends TherminalError {\n static override defaultErrorCode = \"HTTP_FORBIDDEN\";\n\n constructor(message = \"Access denied\", options: TherminalErrorOptions = {}) {\n super(message, { ...options, statusCode: options.statusCode ?? 403 });\n }\n}\n\nexport class ServerError extends TherminalError {\n static override defaultErrorCode = \"HTTP_SERVER_ERROR\";\n\n constructor(message = \"Server error\", options: TherminalErrorOptions = {}) {\n super(message, { ...options, statusCode: options.statusCode ?? 500 });\n }\n}\n\n// ---------------------------------------------------------------------------\n// LiveStreamError + NoLiveDataError (Phase 11)\n// ---------------------------------------------------------------------------\n\n/**\n * Base class for `mostlyright.live.stream` / `live.latest` failures.\n *\n * Mirrors Python `LiveStreamError`. Live-streaming errors are a separate\n * sub-tree from `SourceUnavailableError` because the recovery path differs —\n * `stream()` swallows empty-tick errors and waits for the next polite-floor\n * cycle. Only `latest()` raises `NoLiveDataError` on empty responses.\n */\nexport class LiveStreamError extends TradewindsError {\n static override defaultErrorCode = \"LIVE_STREAM_ERROR\";\n}\n\nexport interface NoLiveDataErrorOptions extends TradewindsErrorOptions {\n station: string;\n source: string;\n}\n\n/**\n * `mostlyright.live.latest` returned no observations for the station.\n *\n * Carries the resolved ICAO `station` and the canonical source identity\n * tag (`\"awc.live\"` / `\"iem.live\"`) so caller logs can branch by source\n * without re-parsing the message.\n */\nexport class NoLiveDataError extends LiveStreamError {\n static override defaultErrorCode = \"NO_LIVE_DATA\";\n\n readonly station: string;\n\n constructor(message = \"\", options: NoLiveDataErrorOptions) {\n super(message, { ...options, source: options.source });\n this.station = options.station;\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n station: this.station,\n };\n }\n}\n","// KnowledgeView — temporal filtering by `knowledge_time`.\n//\n// Mirrors `packages/core/src/mostlyright/core/temporal/knowledge_view.py`.\n// A plain class (not an iterator subclass, not a DataFrame accessor) that\n// filters its `rows()` output to entries where `knowledge_time <= asOf`.\n//\n// Construction validates the shape of EVERY row up-front: each\n// `knowledge_time` must be a string that TimePoint accepts (rejecting naive\n// / date-only / NaN inputs). Any failure throws SchemaValidationError with\n// the violating row indices in `violations`.\n//\n// Iter-1 H2: violation `rule` vocabulary matches Python EXACTLY for MCP\n// cross-language serialization parity:\n//\n// - \"required\" — missing column / non-string knowledge_time\n// - \"tz_aware_utc\" — naive or otherwise invalid tz-aware ISO string\n//\n// See docs/design.md §M for the leakage-free training-view semantics.\n\nimport { SchemaValidationError } from \"../exceptions/index.js\";\nimport { TimePoint } from \"./timepoint.js\";\n\n/**\n * A filtered, knowledge-time-bounded view over an array of rows.\n *\n * @template Row — caller's row type. Must have a string `knowledge_time` field.\n */\nexport class KnowledgeView<Row extends { knowledge_time: string }> {\n readonly #rows: ReadonlyArray<Row>;\n readonly #asOf: TimePoint;\n // Iter-11 C13: cache the asOf epoch-µs (BigInt) for filter comparisons.\n // Previously cached `asOfMs` (epoch-ms) and compared via `Date.parse`,\n // which collapsed `.123456Z` and `.123789Z` to the same instant — a\n // µs-resolution row \"known\" after asOf could slip through.\n readonly #asOfMicros: bigint;\n\n constructor(rows: ReadonlyArray<Row>, asOf: TimePoint) {\n if (!(asOf instanceof TimePoint)) {\n throw new TypeError(`asOf must be a TimePoint; got ${typeof asOf}`);\n }\n // Eager per-row validation: every row's knowledge_time must parse as a\n // valid tz-aware ISO datetime. We piggyback on TimePoint's rejection\n // logic so the error vocabulary matches construction-time errors.\n const violations: Array<Record<string, unknown>> = [];\n for (let i = 0; i < rows.length; i++) {\n const r = rows[i];\n if (r == null || typeof r.knowledge_time !== \"string\") {\n // H2: Python `knowledge_view.py` uses `rule=\"required\"` when the\n // knowledge_time column is missing or wrong-typed. Mirror the\n // vocabulary so MCP wire payloads match cross-language.\n violations.push({\n column: \"knowledge_time\",\n row_idx: i,\n rule: \"required\",\n });\n continue;\n }\n try {\n new TimePoint(r.knowledge_time);\n } catch (e) {\n // H2: Python uses `rule=\"tz_aware_utc\"` for naive / invalid\n // datetime strings (the upstream Python check is on the column\n // dtype, not per-row, but the rule string is what the wire\n // payload pins). Preserve the parsed error message in `message`\n // for debugging.\n violations.push({\n column: \"knowledge_time\",\n row_idx: i,\n rule: \"tz_aware_utc\",\n message: String(e),\n });\n }\n }\n if (violations.length > 0) {\n throw new SchemaValidationError(\n `KnowledgeView received ${violations.length} row(s) with invalid knowledge_time`,\n { schemaId: \"<runtime>\", violations },\n );\n }\n this.#rows = rows;\n this.#asOf = asOf;\n this.#asOfMicros = asOf.toEpochMicros();\n }\n\n /**\n * Return a freshly filtered array — only rows where\n * `knowledge_time <= asOf`. The returned array is a NEW reference each\n * call (defensive copy semantics), so callers can mutate it without\n * affecting subsequent calls.\n */\n rows(): ReadonlyArray<Row> {\n // Iter-11 C13: per-row comparison goes through TimePoint so we get\n // epoch-µs precision. Constructing a TimePoint per row is more work\n // than the previous `Date.parse(...)` shortcut, but the constructor\n // is also the only path that captures 4-6-digit fractional seconds\n // correctly — without it, two distinct µs-resolution rows would\n // compare equal via `Date.parse` (which is ms-only).\n return this.#rows.filter((r) => {\n const ktMicros = new TimePoint(r.knowledge_time).toEpochMicros();\n return ktMicros <= this.#asOfMicros;\n });\n }\n\n /** The as-of cutoff supplied at construction. */\n get asOf(): TimePoint {\n return this.#asOf;\n }\n}\n","// assertNoLeakage + LeakageDetector — loud assertion of as-of leakage absence.\n//\n// Mirrors `packages/core/src/mostlyright/core/temporal/leakage.py`. Where\n// KnowledgeView silently filters, assertNoLeakage throws LeakageError if\n// any row has knowledge_time > asOf. The error payload follows design.md\n// §D: violatingCount + sampleViolations (capped at 10).\n//\n// Behavior parity with Python `leakage.py`:\n// - Missing `knowledge_time` field → SchemaValidationError with\n// `violations: [{ column: \"knowledge_time\", rule: \"required\" }]`.\n// Matches Python's `\"knowledge_time\" not in df.columns` branch.\n// - Non-string `knowledge_time` value → SchemaValidationError with\n// `rule: \"datetime_dtype\"`. Matches Python's\n// `is_datetime64_any_dtype(col)` check.\n// - Naive / tz-less / unparseable `knowledge_time` string →\n// SchemaValidationError with `rule: \"tz_aware_utc\"`. Matches Python's\n// `col.dt.tz is None` check.\n// - Iter-3 C9 fix: previously these three cases were SKIPPED silently\n// (rows quietly dropped from the `>` comparison), which let malformed\n// temporal data pass the leakage gate. The Python contract raises;\n// the TS port now matches.\n// - Sample cap = 10.\n// - Wire format for `as_of` AND `sample_violations[].knowledge_time` uses\n// the Python `datetime.isoformat()` shape (`\"...T12:00:00+00:00\"`) via\n// `TimePoint.toPythonIso()` — iter-1 H1 fix. MCP clients comparing\n// these strings across Python and TS see byte-equivalent values.\n\nimport { LeakageError, SchemaValidationError } from \"../exceptions/index.js\";\nimport { TimePoint } from \"./timepoint.js\";\n\nconst SAMPLE_CAP = 10;\nconst RUNTIME_SCHEMA_ID = \"<runtime>\";\n\n/**\n * Throw {@link LeakageError} if any row's `knowledge_time` is strictly\n * greater than `asOf`. Leak-free input returns void.\n *\n * Loudly rejects rows whose `knowledge_time` is missing, not a string,\n * or not a tz-aware ISO 8601 datetime — these surface as\n * {@link SchemaValidationError} (rule: `required` /\n * `datetime_dtype` / `tz_aware_utc`), mirroring Python's\n * `assert_no_leakage` validation contract.\n *\n * @template Row — caller's row type. Must have a string `knowledge_time` field.\n */\nexport function assertNoLeakage<Row extends { knowledge_time: string }>(\n rows: ReadonlyArray<Row>,\n asOf: TimePoint,\n): void {\n if (!(asOf instanceof TimePoint)) {\n throw new TypeError(`asOf must be a TimePoint; got ${typeof asOf}`);\n }\n // Iter-11 C13: compare via TimePoint.after() (epoch-µs) rather than\n // `toUTCDate().getTime()` (epoch-ms). A row \"known\" 333µs after `asOf`\n // would silently pass the ms-comparison gate; the µs-aware comparison\n // catches it.\n const violations: Array<{ row_idx: number; knowledge_time: string }> = [];\n for (let i = 0; i < rows.length; i++) {\n const r = rows[i];\n // Reject missing rows / missing knowledge_time field (iter-3 C9).\n // Python: `\"knowledge_time\" not in df.columns` →\n // SchemaValidationError(rule=\"required\", column=\"knowledge_time\")\n if (r == null) {\n throw new SchemaValidationError(\"assertNoLeakage requires 'knowledge_time' on every row\", {\n schemaId: RUNTIME_SCHEMA_ID,\n violations: [{ column: \"knowledge_time\", rule: \"required\", row_idx: i }],\n quarantineCount: rows.length,\n sampleViolations: [{ column: \"knowledge_time\", rule: \"required\", row_idx: i }],\n });\n }\n const rec = r as unknown as Record<string, unknown>;\n if (!(\"knowledge_time\" in rec) || rec.knowledge_time === undefined) {\n throw new SchemaValidationError(\"assertNoLeakage requires 'knowledge_time' on every row\", {\n schemaId: RUNTIME_SCHEMA_ID,\n violations: [{ column: \"knowledge_time\", rule: \"required\", row_idx: i }],\n quarantineCount: rows.length,\n sampleViolations: [{ column: \"knowledge_time\", rule: \"required\", row_idx: i }],\n });\n }\n // Reject non-string knowledge_time (iter-3 C9). Python: a non-datetime\n // column raises with `rule=\"datetime_dtype\"`. The TS analog is a\n // per-row value that isn't even a string.\n const raw = rec.knowledge_time;\n if (typeof raw !== \"string\") {\n throw new SchemaValidationError(\n `assertNoLeakage requires string knowledge_time; got ${typeof raw} at row ${i}`,\n {\n schemaId: RUNTIME_SCHEMA_ID,\n violations: [{ column: \"knowledge_time\", rule: \"datetime_dtype\", row_idx: i }],\n quarantineCount: rows.length,\n sampleViolations: [{ column: \"knowledge_time\", rule: \"datetime_dtype\", row_idx: i }],\n },\n );\n }\n // Reject naive / unparseable / non-tz-aware strings (iter-3 C9).\n // Python: `col.dt.tz is None` raises with `rule=\"tz_aware_utc\"`.\n // The TS analog: TimePoint's constructor rejects anything that isn't\n // a tz-aware ISO 8601 datetime (date-only, naive, unparseable, etc.).\n // Re-wrap any TimePoint constructor failure as SchemaValidationError\n // with the Python vocab so cross-SDK MCP consumers see the same\n // rule strings.\n let kt: TimePoint;\n try {\n kt = new TimePoint(raw);\n } catch (_err) {\n throw new SchemaValidationError(\n `assertNoLeakage requires tz-aware UTC knowledge_time; got ${JSON.stringify(raw)} at row ${i}`,\n {\n schemaId: RUNTIME_SCHEMA_ID,\n violations: [{ column: \"knowledge_time\", rule: \"tz_aware_utc\", row_idx: i }],\n quarantineCount: rows.length,\n sampleViolations: [{ column: \"knowledge_time\", rule: \"tz_aware_utc\", row_idx: i }],\n },\n );\n }\n // Iter-11 C13: µs-aware comparison. `kt.after(asOf)` uses epoch-µs\n // (BigInt), so a `.123456Z` knowledge_time vs `.123000Z` asOf is\n // correctly flagged as leakage (456µs strictly after asOf), whereas\n // the previous `.getTime() > asOfMs` (epoch-ms) would have missed it.\n if (kt.after(asOf)) {\n // Re-emit knowledge_time in the Python isoformat shape so cross-\n // language MCP consumers see byte-equivalent strings.\n violations.push({ row_idx: i, knowledge_time: kt.toPythonIso() });\n }\n }\n if (violations.length === 0) return;\n throw new LeakageError(`Found ${violations.length} row(s) with knowledge_time > asOf`, {\n asOf: asOf.toPythonIso(),\n violatingCount: violations.length,\n sampleViolations: violations.slice(0, SAMPLE_CAP),\n });\n}\n\n/**\n * Convenience wrapper for repeated leakage checks against a fixed `asOf`.\n *\n * ```ts\n * const detector = new LeakageDetector(asOf);\n * detector.check(trainingRows);\n * detector.check(featureRows);\n * ```\n */\nexport class LeakageDetector {\n readonly #asOf: TimePoint;\n\n constructor(asOf: TimePoint) {\n if (!(asOf instanceof TimePoint)) {\n throw new TypeError(`asOf must be a TimePoint; got ${typeof asOf}`);\n }\n this.#asOf = asOf;\n }\n\n get asOf(): TimePoint {\n return this.#asOf;\n }\n\n check<Row extends { knowledge_time: string }>(rows: ReadonlyArray<Row>): void {\n assertNoLeakage(rows, this.#asOf);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACgCA,IAAM,gBAAgB;AAKtB,IAAM,qBAAqB;AAC3B,IAAM,YAAY;AAOlB,IAAM,kBAAkB;AAMxB,IAAM,qBAAqB;AAepB,IAAM,YAAN,MAAM,WAAU;AAAA;AAAA,EAEZ;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA;AAAA,EAET,YAAY,OAAsB;AAChC,QAAI,iBAAiB,MAAM;AACzB,YAAM,IAAI,MAAM,QAAQ;AACxB,UAAI,CAAC,OAAO,SAAS,CAAC,GAAG;AAEvB,cAAM,IAAI,WAAW,6CAA6C;AAAA,MACpE;AACA,WAAK,OAAO,IAAI,KAAK,CAAC;AAEtB,WAAK,eAAe,OAAO,CAAC,IAAI;AAChC;AAAA,IACF;AACA,QAAI,OAAO,UAAU,UAAU;AAC7B,YAAM,IAAI,UAAU,kDAAkD,OAAO,KAAK,EAAE;AAAA,IACtF;AACA,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,QAAQ,WAAW,GAAG;AACxB,YAAM,IAAI,WAAW,8CAA8C;AAAA,IACrE;AAEA,QAAI,cAAc,KAAK,OAAO,GAAG;AAC/B,YAAM,IAAI;AAAA,QACR,mDAAmD,KAAK;AAAA,UACtD;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAMA,QAAI,CAAC,mBAAmB,KAAK,OAAO,GAAG;AACrC,YAAM,IAAI;AAAA,QACR,mDAAmD,KAAK;AAAA,UACtD;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAEA,QAAI,CAAC,UAAU,KAAK,OAAO,GAAG;AAC5B,YAAM,IAAI;AAAA,QACR,gEAAgE,KAAK;AAAA,UACnE;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AACA,UAAM,SAAS,KAAK,MAAM,OAAO;AACjC,QAAI,CAAC,OAAO,SAAS,MAAM,GAAG;AAC5B,YAAM,IAAI,WAAW,6CAA6C,KAAK,UAAU,KAAK,CAAC,EAAE;AAAA,IAC3F;AAqBA,UAAM,YAAY,gBAAgB,KAAK,OAAO;AAC9C,QAAI,cAAc,MAAM;AACtB,YAAM,UAAU,OAAO,UAAU,CAAC,CAAC;AACnC,YAAM,WAAW,OAAO,UAAU,CAAC,CAAC;AACpC,YAAM,SAAS,OAAO,UAAU,CAAC,CAAC;AASlC,UAAI,YAAY;AAChB,YAAM,UAAU,wBAAwB,KAAK,OAAO;AAMpD,YAAM,KAAK,YAAY,OAAO,SAAY,QAAQ,CAAC;AACnD,UAAI,OAAO,UAAa,OAAO,KAAK;AAElC,cAAM,OAAO,GAAG,WAAW,GAAG,IAAI,KAAK;AACvC,cAAM,OAAO,GAAG,MAAM,CAAC,EAAE,QAAQ,KAAK,EAAE;AACxC,cAAM,KAAK,OAAO,KAAK,MAAM,GAAG,CAAC,CAAC;AAClC,cAAM,KAAK,OAAO,KAAK,MAAM,GAAG,CAAC,CAAC;AAClC,oBAAY,QAAQ,KAAK,KAAK;AAAA,MAChC;AAGA,YAAM,WAAW,SAAS,YAAY;AACtC,YAAM,aAAa,IAAI,KAAK,QAAQ;AACpC,YAAM,cAAc,WAAW,eAAe;AAC9C,YAAM,eAAe,WAAW,YAAY,IAAI;AAChD,YAAM,aAAa,WAAW,WAAW;AACzC,UAAI,gBAAgB,WAAW,iBAAiB,YAAY,eAAe,QAAQ;AACjF,cAAM,IAAI;AAAA,UACR,iDAAiD,KAAK;AAAA,YACpD;AAAA,UACF,CAAC,kBAAkB,UAAU,CAAC,CAAC,sDAAsD,WAAW,IAAI,OAAO,YAAY,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,OAAO,UAAU,EAAE,SAAS,GAAG,GAAG,CAAC;AAAA,QAClL;AAAA,MACF;AAAA,IACF;AACA,SAAK,OAAO,IAAI,KAAK,MAAM;AAiB3B,UAAM,YAAY,mBAAmB,KAAK,OAAO;AACjD,QAAI,cAAc,MAAM;AACtB,YAAM,SAAS,UAAU,CAAC,KAAK;AAI/B,YAAM,cAAc,GAAG,MAAM,SAAS,MAAM,GAAG,CAAC;AAChD,YAAM,mBAAmB,OAAO,WAAW;AAO3C,YAAM,WAAW,OAAO,MAAM;AAC9B,YAAM,iBAAkB,WAAW,QAAS;AAC5C,YAAM,qBAAqB,iBAAiB;AAC5C,WAAK,eAAe,qBAAqB;AAAA,IAC3C,OAAO;AAGL,WAAK,eAAe,OAAO,MAAM,IAAI;AAAA,IACvC;AAAA,EACF;AAAA;AAAA,EAGA,YAAkB;AAChB,WAAO,IAAI,KAAK,KAAK,KAAK,QAAQ,CAAC;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,gBAAwB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,cAAsB;AACpB,WAAO,KAAK,KAAK,YAAY;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqBA,cAAsB;AACpB,UAAM,OAAO,KAAK,KAAK,eAAe;AACtC,UAAM,QAAQ,OAAO,KAAK,KAAK,YAAY,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AACjE,UAAM,MAAM,OAAO,KAAK,KAAK,WAAW,CAAC,EAAE,SAAS,GAAG,GAAG;AAC1D,UAAM,OAAO,OAAO,KAAK,KAAK,YAAY,CAAC,EAAE,SAAS,GAAG,GAAG;AAC5D,UAAM,SAAS,OAAO,KAAK,KAAK,cAAc,CAAC,EAAE,SAAS,GAAG,GAAG;AAChE,UAAM,SAAS,OAAO,KAAK,KAAK,cAAc,CAAC,EAAE,SAAS,GAAG,GAAG;AAChE,UAAM,OAAO,GAAG,IAAI,IAAI,KAAK,IAAI,GAAG,IAAI,IAAI,IAAI,MAAM,IAAI,MAAM;AAchE,UAAM,cAAc;AACpB,QAAI,iBAAiB,KAAK,eAAe;AACzC,QAAI,iBAAiB,GAAI,mBAAkB;AAC3C,QAAI,mBAAmB,IAAI;AACzB,aAAO,GAAG,IAAI;AAAA,IAChB;AACA,UAAM,SAAS,OAAO,cAAc,EAAE,SAAS,GAAG,GAAG;AACrD,WAAO,GAAG,IAAI,IAAI,MAAM;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,OAAO,IAAoB;AACzB,WAAO,IAAI,KAAK,eAAe,SAAS;AAAA,MACtC,UAAU;AAAA,MACV,MAAM;AAAA,MACN,OAAO;AAAA,MACP,KAAK;AAAA,MACL,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV,CAAC,EAAE,OAAO,KAAK,IAAI;AAAA,EACrB;AAAA,EAEA,OAAO,OAA2B;AAKhC,WAAO,KAAK,iBAAiB,MAAM;AAAA,EACrC;AAAA,EAEA,OAAO,OAA2B;AAChC,WAAO,KAAK,eAAe,MAAM;AAAA,EACnC;AAAA,EAEA,MAAM,OAA2B;AAC/B,WAAO,KAAK,eAAe,MAAM;AAAA,EACnC;AAAA;AAAA,EAGA,OAAO,MAAiB;AACtB,WAAO,IAAI,WAAU,oBAAI,KAAK,CAAC;AAAA,EACjC;AACF;;;AChUO,SAAS,WAAW,OAAgB,MAAiC;AAC1E,QAAM,UAAU,QAAQ,oBAAI,QAAgB;AAG5C,MAAI,UAAU,QAAQ,UAAU,QAAW;AACzC,WAAO;AAAA,EACT;AAGA,MAAI,OAAO,UAAU,WAAW;AAC9B,WAAO;AAAA,EACT;AAGA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO;AAAA,EACT;AAGA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AAAA,EAC1C;AAGA,MAAI,OAAO,UAAU,UAAU;AAE7B,QAAI,SAAS,OAAO,OAAO,gBAAgB,KAAK,SAAS,OAAO,OAAO,gBAAgB,GAAG;AACxF,aAAO,OAAO,KAAK;AAAA,IACrB;AACA,WAAO,EAAE,YAAY,MAAM,OAAO,MAAM,SAAS,EAAE;AAAA,EACrD;AAGA,MAAI,iBAAiB,MAAM;AACzB,QAAI,OAAO,MAAM,MAAM,QAAQ,CAAC,GAAG;AACjC,aAAO;AAAA,IACT;AACA,WAAO,MAAM,YAAY;AAAA,EAC3B;AAGA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,QAAI,QAAQ,IAAI,KAAK,GAAG;AACtB,aAAO,EAAE,QAAQ,MAAM,OAAO,OAAO,KAAK,EAAE;AAAA,IAC9C;AACA,YAAQ,IAAI,KAAK;AACjB,QAAI;AACF,aAAO,MAAM,IAAI,CAAC,SAAS,WAAW,MAAM,OAAO,CAAC;AAAA,IACtD,UAAE;AACA,cAAQ,OAAO,KAAK;AAAA,IACtB;AAAA,EACF;AAGA,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI,QAAQ,IAAI,KAAe,GAAG;AAChC,aAAO,EAAE,QAAQ,MAAM,OAAO,OAAO,KAAK,EAAE;AAAA,IAC9C;AACA,YAAQ,IAAI,KAAe;AAC3B,QAAI;AACF,YAAM,MAA+B,CAAC;AACtC,iBAAW,OAAO,OAAO,KAAK,KAAgC,GAAG;AAC/D,YAAI,OAAO,QAAQ,UAAU;AAC3B,gBAAM,IAAI,UAAU,4CAA4C,OAAO,GAAG,EAAE;AAAA,QAC9E;AACA,YAAI,GAAG,IAAI,WAAY,MAAkC,GAAG,GAAG,OAAO;AAAA,MACxE;AACA,aAAO;AAAA,IACT,UAAE;AACA,cAAQ,OAAO,KAAe;AAAA,IAChC;AAAA,EACF;AAGA,SAAO,EAAE,YAAY,MAAM,OAAO,OAAO,KAAK,EAAE;AAClD;AAoBO,IAAM,kBAAN,cAA8B,MAAM;AAAA;AAAA,EAEzC,OAAO,mBAAmB;AAAA,EAEjB;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,UAAU,IAAI,UAAkC,CAAC,GAAG;AAC9D,UAAM,OAAO;AACb,SAAK,OAAO,WAAW;AACvB,UAAM,OAAO,KAAK;AAClB,SAAK,YAAY,QAAQ,aAAa,KAAK;AAC3C,SAAK,SAAS,QAAQ,UAAU;AAChC,SAAK,YAAY,QAAQ,aAAa;AAGtC,WAAO,eAAe,MAAM,WAAW,SAAS;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOU,UAAmC;AAC3C,WAAO;AAAA,MACL,YAAY,KAAK;AAAA,MACjB,SAAS,KAAK;AAAA,MACd,QAAQ,KAAK;AAAA,MACb,YAAY,KAAK;AAAA,IACnB;AAAA,EACF;AAAA;AAAA,EAGA,SAAkC;AAChC,UAAM,OAAO,WAAW,KAAK,QAAQ,CAAC;AACtC,WAAO;AAAA,EACT;AACF;AAuDO,IAAM,wBAAN,cAAoC,gBAAgB;AAAA,EACzD,OAAgB,mBAAmB;AAAA,EAE1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,SAAiB,SAAuC;AAClE,UAAM,SAAS,OAAO;AACtB,SAAK,WAAW,QAAQ;AACxB,SAAK,aAAa,CAAC,GAAI,QAAQ,cAAc,CAAC,CAAE;AAChD,SAAK,kBAAkB,QAAQ,mBAAmB;AAClD,SAAK,mBAAmB,CAAC,GAAI,QAAQ,oBAAoB,CAAC,CAAE;AAAA,EAC9D;AAAA,EAEmB,UAAmC;AACpD,WAAO;AAAA,MACL,GAAG,MAAM,QAAQ;AAAA,MACjB,WAAW,KAAK;AAAA,MAChB,YAAY,KAAK;AAAA,MACjB,kBAAkB,KAAK;AAAA,MACvB,mBAAmB,KAAK;AAAA,IAC1B;AAAA,EACF;AACF;AA2DO,IAAM,eAAN,cAA2B,gBAAgB;AAAA,EAChD,OAAgB,mBAAmB;AAAA,EAE1B;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,SAAiB,SAA8B;AACzD,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO,QAAQ;AACpB,SAAK,iBAAiB,QAAQ;AAC9B,SAAK,mBAAmB,CAAC,GAAI,QAAQ,oBAAoB,CAAC,CAAE;AAAA,EAC9D;AAAA,EAEmB,UAAmC;AACpD,WAAO;AAAA,MACL,GAAG,MAAM,QAAQ;AAAA,MACjB,OAAO,KAAK;AAAA,MACZ,iBAAiB,KAAK;AAAA,MACtB,mBAAmB,KAAK;AAAA,IAC1B;AAAA,EACF;AACF;;;ACtSO,IAAM,gBAAN,MAA4D;AAAA,EACxD;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA;AAAA,EAET,YAAY,MAA0B,MAAiB;AACrD,QAAI,EAAE,gBAAgB,YAAY;AAChC,YAAM,IAAI,UAAU,iCAAiC,OAAO,IAAI,EAAE;AAAA,IACpE;AAIA,UAAM,aAA6C,CAAC;AACpD,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,YAAM,IAAI,KAAK,CAAC;AAChB,UAAI,KAAK,QAAQ,OAAO,EAAE,mBAAmB,UAAU;AAIrD,mBAAW,KAAK;AAAA,UACd,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,MAAM;AAAA,QACR,CAAC;AACD;AAAA,MACF;AACA,UAAI;AACF,YAAI,UAAU,EAAE,cAAc;AAAA,MAChC,SAAS,GAAG;AAMV,mBAAW,KAAK;AAAA,UACd,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,MAAM;AAAA,UACN,SAAS,OAAO,CAAC;AAAA,QACnB,CAAC;AAAA,MACH;AAAA,IACF;AACA,QAAI,WAAW,SAAS,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,0BAA0B,WAAW,MAAM;AAAA,QAC3C,EAAE,UAAU,aAAa,WAAW;AAAA,MACtC;AAAA,IACF;AACA,SAAK,QAAQ;AACb,SAAK,QAAQ;AACb,SAAK,cAAc,KAAK,cAAc;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,OAA2B;AAOzB,WAAO,KAAK,MAAM,OAAO,CAAC,MAAM;AAC9B,YAAM,WAAW,IAAI,UAAU,EAAE,cAAc,EAAE,cAAc;AAC/D,aAAO,YAAY,KAAK;AAAA,IAC1B,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,IAAI,OAAkB;AACpB,WAAO,KAAK;AAAA,EACd;AACF;;;AC7EA,IAAM,aAAa;AACnB,IAAM,oBAAoB;AAcnB,SAAS,gBACd,MACA,MACM;AACN,MAAI,EAAE,gBAAgB,YAAY;AAChC,UAAM,IAAI,UAAU,iCAAiC,OAAO,IAAI,EAAE;AAAA,EACpE;AAKA,QAAM,aAAiE,CAAC;AACxE,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,IAAI,KAAK,CAAC;AAIhB,QAAI,KAAK,MAAM;AACb,YAAM,IAAI,sBAAsB,0DAA0D;AAAA,QACxF,UAAU;AAAA,QACV,YAAY,CAAC,EAAE,QAAQ,kBAAkB,MAAM,YAAY,SAAS,EAAE,CAAC;AAAA,QACvE,iBAAiB,KAAK;AAAA,QACtB,kBAAkB,CAAC,EAAE,QAAQ,kBAAkB,MAAM,YAAY,SAAS,EAAE,CAAC;AAAA,MAC/E,CAAC;AAAA,IACH;AACA,UAAM,MAAM;AACZ,QAAI,EAAE,oBAAoB,QAAQ,IAAI,mBAAmB,QAAW;AAClE,YAAM,IAAI,sBAAsB,0DAA0D;AAAA,QACxF,UAAU;AAAA,QACV,YAAY,CAAC,EAAE,QAAQ,kBAAkB,MAAM,YAAY,SAAS,EAAE,CAAC;AAAA,QACvE,iBAAiB,KAAK;AAAA,QACtB,kBAAkB,CAAC,EAAE,QAAQ,kBAAkB,MAAM,YAAY,SAAS,EAAE,CAAC;AAAA,MAC/E,CAAC;AAAA,IACH;AAIA,UAAM,MAAM,IAAI;AAChB,QAAI,OAAO,QAAQ,UAAU;AAC3B,YAAM,IAAI;AAAA,QACR,uDAAuD,OAAO,GAAG,WAAW,CAAC;AAAA,QAC7E;AAAA,UACE,UAAU;AAAA,UACV,YAAY,CAAC,EAAE,QAAQ,kBAAkB,MAAM,kBAAkB,SAAS,EAAE,CAAC;AAAA,UAC7E,iBAAiB,KAAK;AAAA,UACtB,kBAAkB,CAAC,EAAE,QAAQ,kBAAkB,MAAM,kBAAkB,SAAS,EAAE,CAAC;AAAA,QACrF;AAAA,MACF;AAAA,IACF;AAQA,QAAI;AACJ,QAAI;AACF,WAAK,IAAI,UAAU,GAAG;AAAA,IACxB,SAAS,MAAM;AACb,YAAM,IAAI;AAAA,QACR,6DAA6D,KAAK,UAAU,GAAG,CAAC,WAAW,CAAC;AAAA,QAC5F;AAAA,UACE,UAAU;AAAA,UACV,YAAY,CAAC,EAAE,QAAQ,kBAAkB,MAAM,gBAAgB,SAAS,EAAE,CAAC;AAAA,UAC3E,iBAAiB,KAAK;AAAA,UACtB,kBAAkB,CAAC,EAAE,QAAQ,kBAAkB,MAAM,gBAAgB,SAAS,EAAE,CAAC;AAAA,QACnF;AAAA,MACF;AAAA,IACF;AAKA,QAAI,GAAG,MAAM,IAAI,GAAG;AAGlB,iBAAW,KAAK,EAAE,SAAS,GAAG,gBAAgB,GAAG,YAAY,EAAE,CAAC;AAAA,IAClE;AAAA,EACF;AACA,MAAI,WAAW,WAAW,EAAG;AAC7B,QAAM,IAAI,aAAa,SAAS,WAAW,MAAM,sCAAsC;AAAA,IACrF,MAAM,KAAK,YAAY;AAAA,IACvB,gBAAgB,WAAW;AAAA,IAC3B,kBAAkB,WAAW,MAAM,GAAG,UAAU;AAAA,EAClD,CAAC;AACH;AAWO,IAAM,kBAAN,MAAsB;AAAA,EAClB;AAAA,EAET,YAAY,MAAiB;AAC3B,QAAI,EAAE,gBAAgB,YAAY;AAChC,YAAM,IAAI,UAAU,iCAAiC,OAAO,IAAI,EAAE;AAAA,IACpE;AACA,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,IAAI,OAAkB;AACpB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAA8C,MAAgC;AAC5E,oBAAgB,MAAM,KAAK,KAAK;AAAA,EAClC;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/temporal/index.ts","../../src/temporal/timepoint.ts","../../src/exceptions/index.ts","../../src/temporal/knowledge-view.ts","../../src/temporal/leakage.ts"],"sourcesContent":["// Barrel for @mostlyrightmd/core/temporal — TS-W3 Plan 04.\n//\n// Public API: TimePoint, KnowledgeView, LeakageDetector, assertNoLeakage.\n\nexport { TimePoint } from \"./timepoint.js\";\nexport { KnowledgeView } from \"./knowledge-view.js\";\nexport { LeakageDetector, assertNoLeakage } from \"./leakage.js\";\n","// UTC-aware timestamp wrapper for mostlyright.core (TS-W3 Plan 04 Task 1).\n//\n// Mirrors `packages/core/src/mostlyright/core/temporal/timepoint.py`.\n// Every timestamp in mostlyright.core is UTC-aware. TimePoint rejects:\n// - naive ISO strings (no Z, no +HH:MM, no -HH:MM offset)\n// - date-only ISO strings (e.g. \"2026-05-21\" — no time component)\n// - date-only ISO strings with a timezone suffix (e.g. \"2026-05-21Z\" or\n// \"2026-05-21+00:00\") — Python `_from_iso_string` rejects these by\n// requiring a `T` (or space) separator BEFORE checking the tz suffix.\n// - NaN / Infinity / -Infinity (via Date(NaN), Date(Infinity), etc.)\n// - empty / whitespace-only strings\n// - non-Date, non-string inputs (TypeError; belt-and-suspenders runtime guard)\n//\n// Note on Date inputs: a JS `Date` is just epoch ms — no timezone metadata.\n// \"Naive\" only applies to STRING inputs, where we can inspect the source\n// text for a timezone indicator. For Date inputs we only reject NaN/Infinity.\n//\n// Iter-11 C13: TimePoint preserves MICROSECOND precision when constructed\n// from a string with 4-6 digit fractional seconds. Internally we still\n// carry a `Date` (epoch-ms, used for `toUTCDate()` and `asZone()` since\n// JS Date has no µs precision), but we ALSO carry a `bigint` epoch-µs\n// field used for ALL equality / before / after comparisons. Without this,\n// `Date.parse(\"2025-01-02T12:00:00.123456Z\")` collapses to the same\n// epoch-ms as `.123789Z`, and `assertNoLeakage()` would silently miss a\n// row \"known\" 333µs after `asOf` — a temporal-safety violation.\n//\n// BigInt is fully supported in all target runtimes (Node 16+, modern\n// browsers, MV3 service workers per ES2020). The Date class is left\n// ms-only on purpose — that's fine for display/timezone operations, and\n// `toUTCDate()` deliberately returns ms precision for back-compat. The\n// µs precision is for comparison/equality only.\n\nconst ISO_DATE_ONLY = /^\\d{4}-\\d{2}-\\d{2}$/;\n// ISO 8601 requires a `T` (or RFC 3339 space) separator between the date\n// and time components. Without one, the string is a bare date — even if a\n// timezone suffix is appended (`\"2026-05-21Z\"`). Mirrors the Python guard\n// in `_from_iso_string` that checks `\"T\" not in s and \" \" not in s`.\nconst DATETIME_SEPARATOR = /[T ]/;\nconst TZ_SUFFIX = /(?:Z|[+-]\\d{2}:?\\d{2})$/;\n// Calendar-validity check (iter-3 C8): extract the YYYY-MM-DD prefix so we\n// can verify that the parsed UTC date matches the literal date the caller\n// supplied. `Date.parse(\"2025-02-30T00:00:00Z\")` silently normalizes to\n// `2025-03-02T00:00:00.000Z`, while Python `datetime.fromisoformat(...)`\n// raises ValueError. Mirror the Python contract by rejecting any string\n// whose calendar fields don't survive the round-trip.\nconst ISO_DATE_PREFIX = /^(\\d{4})-(\\d{2})-(\\d{2})/;\n// Iter-11 C13: extract the fractional-seconds digits between the seconds\n// field and the tz suffix. Captures 1-9 digits; Python `fromisoformat`\n// truncates beyond 6 (microseconds), so we do the same. Anchored at a\n// `.` after the `HH:MM:SS` segment and bounded by the start of the tz\n// suffix (`Z`, `+`, or `-`).\nconst FRACTIONAL_SECONDS = /[T ]\\d{2}:\\d{2}:\\d{2}\\.(\\d+)(?:Z|[+-])/;\n\n/**\n * UTC-aware timestamp wrapper.\n *\n * Equivalent to Python's `mostlyright.core.TimePoint`. Constructed from either\n * a `Date` (rejects NaN/Infinity) or a tz-aware ISO 8601 string (rejects\n * naive / date-only inputs). Internally stored as an epoch-ms Date (for\n * display/timezone ops) PLUS an epoch-µs `bigint` (used for comparisons).\n *\n * Immutable: the underlying Date is hidden behind a private `#utc` field\n * and `toUTCDate()` returns a defensive copy so callers cannot mutate the\n * wrapped instant. The `#epochMicros` field carries full µs precision\n * captured from string inputs with 4-6 digit fractional seconds.\n */\nexport class TimePoint {\n // Private fields — naive ISO strings cannot leak back through any accessor.\n readonly #utc: Date;\n // Iter-11 C13: full µs precision (epoch microseconds). For Date inputs\n // and string inputs with ≤3 fractional digits this is just\n // `#utc.getTime() * 1000n`. For string inputs with 4-6 fractional digits\n // we capture the true µs value from the source string.\n readonly #epochMicros: bigint;\n\n constructor(value: Date | string) {\n if (value instanceof Date) {\n const t = value.getTime();\n if (!Number.isFinite(t)) {\n // RangeError for NaN/Infinity — see \"date-only\" / \"naive\" peers below.\n throw new RangeError(\"TimePoint does not accept NaN/Infinity Date\");\n }\n this.#utc = new Date(t);\n // Date inputs only carry ms precision — derive µs trivially.\n this.#epochMicros = BigInt(t) * 1000n;\n return;\n }\n if (typeof value !== \"string\") {\n throw new TypeError(`TimePoint accepts Date or ISO 8601 string; got ${typeof value}`);\n }\n const trimmed = value.trim();\n if (trimmed.length === 0) {\n throw new RangeError(\"TimePoint requires non-empty ISO 8601 string\");\n }\n // Date-only check: a bare \"YYYY-MM-DD\" has no time component.\n if (ISO_DATE_ONLY.test(trimmed)) {\n throw new RangeError(\n `TimePoint requires datetime, not date-only (got ${JSON.stringify(\n value,\n )}). Use an ISO 8601 datetime with a timezone, e.g. '2026-05-21T14:30:00Z'.`,\n );\n }\n // Separator check (MUST run BEFORE the tz-suffix check): a date-only\n // payload like \"2026-05-21Z\" or \"2026-05-21+00:00\" has no `T`/space\n // separator and is still a bare date — Date.parse silently normalizes\n // it to midnight UTC, but Python `_from_iso_string` rejects it. Reject\n // here so the constructor matches the Python contract exactly.\n if (!DATETIME_SEPARATOR.test(trimmed)) {\n throw new RangeError(\n `TimePoint requires datetime, not date-only (got ${JSON.stringify(\n value,\n )}). ISO 8601 requires a 'T' or space separator between the date and time, e.g. '2026-05-21T14:30:00Z'.`,\n );\n }\n // Naive check: must end in Z, +HH:MM, +HHMM, -HH:MM, or -HHMM.\n if (!TZ_SUFFIX.test(trimmed)) {\n throw new RangeError(\n `TimePoint requires tz-aware timestamp; got naive ISO string (${JSON.stringify(\n value,\n )}). Include a timezone offset (e.g. 'Z' or '+00:00').`,\n );\n }\n const parsed = Date.parse(trimmed);\n if (!Number.isFinite(parsed)) {\n throw new RangeError(`TimePoint could not parse ISO 8601 string ${JSON.stringify(value)}`);\n }\n // Calendar-validity check (iter-3 C8): `Date.parse` is forgiving — it\n // silently normalizes impossible dates (e.g. \"2025-02-30T...\" becomes\n // \"2025-03-02T...\", \"2025-13-01T...\" becomes \"2026-01-01T...\"). Python\n // `datetime.fromisoformat` raises ValueError for the same inputs. To\n // match the Python contract, extract the YYYY-MM-DD prefix from the\n // ORIGINAL trimmed input and require that the parsed Date's UTC\n // year/month/day match exactly. Any mismatch means Date.parse rolled\n // the date over — reject loudly. The parsed date's UTC fields are the\n // right basis for comparison because non-UTC tz-suffixes (e.g.\n // \"...T23:00:00-05:00\") legitimately shift the wall-clock date forward\n // when converted to UTC, but only by ±1 day; an off-by-one shift due\n // to a legitimate tz offset will still hit this check, so we ALSO\n // accept a match against the local fields the source string asserts.\n //\n // Strategy: compute what the source-side year/month/day would be after\n // applying the declared UTC offset, then compare those derived fields\n // against the literal YYYY-MM-DD in the string. If the source string\n // was an impossible calendar date, Date.parse's silent normalization\n // will have shifted the underlying ms beyond what the declared offset\n // alone could account for, so the derived fields won't match.\n const dateMatch = ISO_DATE_PREFIX.exec(trimmed);\n if (dateMatch !== null) {\n const litYear = Number(dateMatch[1]);\n const litMonth = Number(dateMatch[2]);\n const litDay = Number(dateMatch[3]);\n // Derive the source-side date by undoing the declared tz offset.\n // The trailing tz suffix is one of: \"Z\", \"+HH:MM\", \"-HH:MM\", \"+HHMM\",\n // \"-HHMM\". For \"Z\" the offset is 0. For the signed forms, the offset\n // is the source's distance ahead/behind UTC: a \"-05:00\" tz on\n // \"10:00:00\" means UTC is 15:00:00, so to recover the source's\n // wall-clock year/month/day we ADD the offset back to the UTC ms\n // before extracting Y/M/D. Equivalently: build a Date at the same\n // ms, then read its UTC fields after offsetting.\n let offsetMin = 0;\n const tzMatch = /(Z|[+-]\\d{2}:?\\d{2})$/.exec(trimmed);\n // Under `noUncheckedIndexedAccess: true`, `tzMatch[1]` is typed\n // `string | undefined` even after the `null` guard. Capture it once\n // and narrow with an explicit undefined check (unreachable when\n // tzMatch is non-null — the regex always populates group 1 — but\n // the type checker can't prove that on its own).\n const tz = tzMatch === null ? undefined : tzMatch[1];\n if (tz !== undefined && tz !== \"Z\") {\n // tz is like \"+05:30\", \"-0500\", \"+0000\".\n const sign = tz.startsWith(\"-\") ? -1 : 1;\n const body = tz.slice(1).replace(\":\", \"\");\n const hh = Number(body.slice(0, 2));\n const mm = Number(body.slice(2, 4));\n offsetMin = sign * (hh * 60 + mm);\n }\n // The source's wall-clock instant in ms-since-epoch: take the UTC ms\n // and add the offset (positive offset means source is ahead of UTC).\n const sourceMs = parsed + offsetMin * 60_000;\n const sourceDate = new Date(sourceMs);\n const derivedYear = sourceDate.getUTCFullYear();\n const derivedMonth = sourceDate.getUTCMonth() + 1;\n const derivedDay = sourceDate.getUTCDate();\n if (derivedYear !== litYear || derivedMonth !== litMonth || derivedDay !== litDay) {\n throw new RangeError(\n `TimePoint rejects impossible calendar date in ${JSON.stringify(\n value,\n )}: literal date ${dateMatch[0]} does not survive round-trip (parser normalized to ${derivedYear}-${String(derivedMonth).padStart(2, \"0\")}-${String(derivedDay).padStart(2, \"0\")}). Python datetime.fromisoformat raises ValueError on this input.`,\n );\n }\n }\n this.#utc = new Date(parsed);\n // Iter-11 C13: capture full µs precision from the source string.\n //\n // `Date.parse` truncates fractional seconds to ms (3 digits). To\n // recover µs precision when the source carries 4-6 digits, we scan\n // the trimmed string for the `.<digits>` segment between `HH:MM:SS`\n // and the tz suffix, then convert the captured digits to a µs\n // remainder added on top of the seconds-aligned epoch-µs base.\n //\n // The \"base\" is the whole-seconds portion of the parsed UTC ms:\n // `floor(parsed / 1000) * 1_000_000n`. We then add the fractional\n // part as µs. This avoids relying on `Date.parse`'s lossy ms-only\n // fractional handling entirely. Python `datetime.fromisoformat`\n // accepts arbitrary fractional precision but truncates to 6 digits\n // (µs) — we mirror that: take only the first 6 digits, ignore the\n // rest. Anything past digit 6 is below µs precision and must NOT\n // round up (Python truncates; we match).\n const fracMatch = FRACTIONAL_SECONDS.exec(trimmed);\n if (fracMatch !== null) {\n const digits = fracMatch[1] ?? \"\";\n // Pad to 6 (so \".1\" → \"100000\" µs) and truncate to 6 (so\n // \".1234567\" → \"123456\" µs; the 7th digit is sub-µs and dropped,\n // matching Python's behavior).\n const microDigits = `${digits}000000`.slice(0, 6);\n const fractionalMicros = BigInt(microDigits);\n // Whole-second floor of the parsed UTC instant, in µs. Use BigInt\n // arithmetic throughout so we don't lose precision crossing the\n // ±2^53 ms threshold (parsed is in ms; Math.floor on a regular\n // number is still safe in the ms range, but the conversion to µs\n // — multiply by 1e6 — would NOT be safe as a regular number for\n // any realistic timestamp, so do the math in BigInt).\n const parsedMs = BigInt(parsed);\n const wholeSecondsMs = (parsedMs / 1000n) * 1000n;\n const wholeSecondsMicros = wholeSecondsMs * 1000n;\n this.#epochMicros = wholeSecondsMicros + fractionalMicros;\n } else {\n // No fractional component in the source string — the ms-precision\n // `parsed` value is already the full truth.\n this.#epochMicros = BigInt(parsed) * 1000n;\n }\n }\n\n /** Return a defensive copy of the underlying UTC Date. */\n toUTCDate(): Date {\n return new Date(this.#utc.getTime());\n }\n\n /**\n * Return the epoch microseconds as a `bigint`.\n *\n * Iter-11 C13: this is the comparison-safe accessor. JS `Date` only\n * carries ms precision, so callers comparing `toUTCDate().getTime()`\n * across two TimePoints constructed from `.123456Z` and `.123789Z`\n * would see them as equal — they're not. Use `equals`/`before`/`after`\n * (or this accessor directly) to compare with full µs precision.\n */\n toEpochMicros(): bigint {\n return this.#epochMicros;\n }\n\n /** Return the canonical ISO 8601 UTC string (always ends in 'Z'). */\n toISOString(): string {\n return this.#utc.toISOString();\n }\n\n /**\n * Return the Python-compatible ISO 8601 UTC string.\n *\n * Where {@link toISOString} emits the JS-native `\"...Z\"` suffix and\n * forces `.000` millisecond padding, this method matches Python's\n * `datetime.isoformat()` output for a UTC-tz-aware datetime:\n *\n * - tz suffix is `+00:00`, never `Z`\n * - subsecond portion is omitted when zero (`\"...T12:00:00+00:00\"`)\n * - microsecond portion appears as 6 digits when non-zero\n * (`\"...T12:00:00.123456+00:00\"`); built from the µs-precision\n * `#epochMicros` field so 4-6 digit fractional inputs round-trip\n * exactly (iter-11 C13 fix).\n *\n * Used by error payloads (LeakageError.toDict, sample violations) so\n * MCP clients comparing the on-wire string across the Python and TS\n * SDKs see byte-equivalent values. Iter-1 H1 fix; iter-11 C13 extends\n * to full µs precision.\n */\n toPythonIso(): string {\n const year = this.#utc.getUTCFullYear();\n const month = String(this.#utc.getUTCMonth() + 1).padStart(2, \"0\");\n const day = String(this.#utc.getUTCDate()).padStart(2, \"0\");\n const hour = String(this.#utc.getUTCHours()).padStart(2, \"0\");\n const minute = String(this.#utc.getUTCMinutes()).padStart(2, \"0\");\n const second = String(this.#utc.getUTCSeconds()).padStart(2, \"0\");\n const head = `${year}-${month}-${day}T${hour}:${minute}:${second}`;\n // Iter-11 C13: derive the µs fraction from `#epochMicros`, not from\n // `Date.getUTCMilliseconds()`. The Date is ms-only, so for a source\n // like \".123456Z\" the Date's ms field is 123 — emitting that as\n // \".123000+00:00\" silently drops 456µs. The µs field carries the\n // full truth.\n //\n // `#epochMicros % 1_000_000n` is the µs-within-the-second component\n // for any non-negative epoch. For pre-epoch timestamps (rare in\n // mostlyright — design.md says UTC and ≥ 2018), BigInt's `%` rounds\n // toward zero, which would produce a negative remainder. Python's\n // `%` rounds toward negative infinity (always non-negative for a\n // positive divisor). Normalize by adding the divisor back if the\n // remainder is negative, so we match Python's behavior at the edge.\n const ONE_MILLION = 1_000_000n;\n let microsInSecond = this.#epochMicros % ONE_MILLION;\n if (microsInSecond < 0n) microsInSecond += ONE_MILLION;\n if (microsInSecond === 0n) {\n return `${head}+00:00`;\n }\n const micros = String(microsInSecond).padStart(6, \"0\");\n return `${head}.${micros}+00:00`;\n }\n\n /**\n * Format this instant in an IANA timezone via Intl.DateTimeFormat.\n * Display helper only — canonical storage stays UTC.\n *\n * Uses `en-CA` locale for a YYYY-MM-DD, HH:MM:SS shape that's easy to grep\n * in logs. The exact output format may vary slightly across Node releases;\n * callers writing tests should use loose contains-style assertions.\n */\n asZone(tz: string): string {\n return new Intl.DateTimeFormat(\"en-CA\", {\n timeZone: tz,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n }).format(this.#utc);\n }\n\n equals(other: TimePoint): boolean {\n // Iter-11 C13: compare epoch-µs, NOT epoch-ms. Two TimePoints from\n // `.123456Z` and `.123789Z` have the same ms but different µs —\n // they're distinct instants and must compare unequal so a \"knowledge\n // 333µs after asOf\" row is correctly flagged as leakage.\n return this.#epochMicros === other.#epochMicros;\n }\n\n before(other: TimePoint): boolean {\n return this.#epochMicros < other.#epochMicros;\n }\n\n after(other: TimePoint): boolean {\n return this.#epochMicros > other.#epochMicros;\n }\n\n /** Return a TimePoint for the current UTC instant. */\n static now(): TimePoint {\n return new TimePoint(new Date());\n }\n}\n","// Structured exception hierarchy for the mostlyright TS SDK.\n//\n// Mirrors the Python design in `packages/core/src/mostlyright/core/exceptions.py`\n// and `packages/core/src/mostlyright/_internal/exceptions.py`. Every error\n// subclasses `TradewindsError` and exposes a `toDict()` method that returns a\n// JSON-safe payload suitable for MCP `error.data` / extension messaging.\n//\n// Role-name vocabulary for SourceMismatchError matches Python:\n// \"observations\" / \"forecasts\" / \"settlement\" (long form, NOT the col prefixes)\n\n// ---------------------------------------------------------------------------\n// JSON-safe coercion (mirrors `mostlyright.core._json_safe.to_json_safe`)\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively coerce `value` into a JSON-serializable structure.\n *\n * Coercion rules (mirrors Python's `to_json_safe`):\n * - `null` / `undefined` / `NaN` / `Infinity` / `-Infinity` → `null`\n * - `Date` → ISO 8601 UTC string ending in `Z`\n * - Numeric / boolean / string scalars pass through (non-finite numbers → null)\n * - Arrays + plain objects → recursive (cycles → `{ _cycle: true, value: String(obj) }`)\n * - Dict keys MUST be strings; non-string keys throw `TypeError`.\n * - Anything else (Symbol, function, class instance without `toJSON`) →\n * `{ _repr_only: true, value: String(value) }`.\n */\nexport function toJsonSafe(value: unknown, seen?: WeakSet<object>): unknown {\n const visited = seen ?? new WeakSet<object>();\n\n // null / undefined → null\n if (value === null || value === undefined) {\n return null;\n }\n\n // Booleans pass through.\n if (typeof value === \"boolean\") {\n return value;\n }\n\n // Strings pass through.\n if (typeof value === \"string\") {\n return value;\n }\n\n // Numbers — non-finite (NaN, +/-Infinity) coerce to null.\n if (typeof value === \"number\") {\n return Number.isFinite(value) ? value : null;\n }\n\n // bigint → number when safe, else stringify (JSON can't natively encode bigint).\n if (typeof value === \"bigint\") {\n // Match Python: integers pass through as native numeric. Safe-range only.\n if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {\n return Number(value);\n }\n return { _repr_only: true, value: value.toString() };\n }\n\n // Date → ISO 8601 UTC string (always ending in \"Z\").\n if (value instanceof Date) {\n if (Number.isNaN(value.getTime())) {\n return null;\n }\n return value.toISOString();\n }\n\n // Arrays — recurse, track cycles.\n if (Array.isArray(value)) {\n if (visited.has(value)) {\n return { _cycle: true, value: String(value) };\n }\n visited.add(value);\n try {\n return value.map((item) => toJsonSafe(item, visited));\n } finally {\n visited.delete(value);\n }\n }\n\n // Plain objects — recurse, track cycles, enforce string keys.\n if (typeof value === \"object\") {\n if (visited.has(value as object)) {\n return { _cycle: true, value: String(value) };\n }\n visited.add(value as object);\n try {\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(value as Record<string, unknown>)) {\n if (typeof key !== \"string\") {\n throw new TypeError(`toJsonSafe dict keys must be string; got ${typeof key}`);\n }\n out[key] = toJsonSafe((value as Record<string, unknown>)[key], visited);\n }\n return out;\n } finally {\n visited.delete(value as object);\n }\n }\n\n // Symbols, functions, etc. — repr-only marker.\n return { _repr_only: true, value: String(value) };\n}\n\n// ---------------------------------------------------------------------------\n// Base class\n// ---------------------------------------------------------------------------\n\nexport interface TradewindsErrorOptions {\n errorCode?: string;\n source?: string | null;\n requestId?: string | null;\n}\n\n/**\n * Base class for all mostlyright structured errors.\n *\n * `errorCode` is a stable enum (e.g. \"SOURCE_UNAVAILABLE\") used by callers /\n * agents to branch on without parsing message text. `source` is the source id\n * involved (e.g. \"iem.archive\") when applicable, and `requestId` correlates a\n * JSON-RPC / MCP request id when applicable.\n */\nexport class TradewindsError extends Error {\n /** Subclass override — the stable string enum surfaced via `errorCode`. */\n static defaultErrorCode = \"TRADEWINDS_ERROR\";\n\n readonly errorCode: string;\n readonly source: string | null;\n readonly requestId: string | null;\n\n constructor(message = \"\", options: TradewindsErrorOptions = {}) {\n super(message);\n this.name = new.target.name;\n const ctor = this.constructor as typeof TradewindsError;\n this.errorCode = options.errorCode ?? ctor.defaultErrorCode;\n this.source = options.source ?? null;\n this.requestId = options.requestId ?? null;\n // Restore prototype chain after `Error` (needed for `instanceof` across\n // transpilation / ES5 targets).\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n /**\n * Subclass hook returning the structured attributes for `toDict`.\n * Values are passed through `toJsonSafe` by `toDict()`, so subclasses\n * don't need to coerce values themselves.\n */\n protected payload(): Record<string, unknown> {\n return {\n error_code: this.errorCode,\n message: this.message,\n source: this.source,\n request_id: this.requestId,\n };\n }\n\n /** Return a JSON-safe dict suitable for MCP `error.data`. */\n toDict(): Record<string, unknown> {\n const safe = toJsonSafe(this.payload());\n return safe as Record<string, unknown>;\n }\n}\n\n// ---------------------------------------------------------------------------\n// SourceUnavailableError\n// ---------------------------------------------------------------------------\n\nexport interface SourceUnavailableErrorOptions extends TradewindsErrorOptions {\n httpStatus?: number | null;\n retryable?: boolean;\n retryAfterS?: number | null;\n underlying?: string;\n url?: string | null;\n}\n\nexport class SourceUnavailableError extends TradewindsError {\n static override defaultErrorCode = \"SOURCE_UNAVAILABLE\";\n\n readonly httpStatus: number | null;\n readonly retryable: boolean;\n readonly retryAfterS: number | null;\n readonly underlying: string;\n readonly url: string | null;\n\n constructor(message = \"\", options: SourceUnavailableErrorOptions = {}) {\n super(message, options);\n this.httpStatus = options.httpStatus ?? null;\n this.retryable = options.retryable ?? false;\n this.retryAfterS = options.retryAfterS ?? null;\n this.underlying = options.underlying ?? \"\";\n this.url = options.url ?? null;\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n http_status: this.httpStatus,\n retryable: this.retryable,\n retry_after_s: this.retryAfterS,\n underlying: this.underlying,\n url: this.url,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// DataAvailabilityError (Phase 21 21-09)\n// ---------------------------------------------------------------------------\n//\n// Typed exception for \"I tried to fetch and got nothing usable\" — replaces\n// 3+ overloaded SourceUnavailableError sites where consumers had to parse\n// message strings to differentiate (rate-limit retry vs model-unavailable\n// hard-fail vs cache-miss re-fetch). The reason enum is shared lockstep\n// with Python (`mostlyright.core.exceptions.DataAvailabilityError`); drift\n// is the load-bearing risk.\n\n/** Shared reason enum — MUST match Python EXACTLY (Phase 21 D-04). */\nexport const DATA_AVAILABILITY_REASONS = [\n \"model_unavailable\",\n \"out_of_window\",\n \"cache_miss\",\n \"source_404\",\n \"source_5xx\",\n \"rate_limited\",\n] as const;\n\nexport type DataAvailabilityReason = (typeof DATA_AVAILABILITY_REASONS)[number];\n\nexport interface DataAvailabilityErrorOptions extends TradewindsErrorOptions {\n reason: DataAvailabilityReason;\n hint: string;\n}\n\nexport class DataAvailabilityError extends TradewindsError {\n static override defaultErrorCode = \"DATA_AVAILABILITY\";\n\n readonly reason: DataAvailabilityReason;\n readonly hint: string;\n\n constructor(options: DataAvailabilityErrorOptions) {\n if (!DATA_AVAILABILITY_REASONS.includes(options.reason)) {\n throw new RangeError(\n `DataAvailabilityError: unknown reason \"${String(options.reason)}\". ` +\n `Valid reasons: ${DATA_AVAILABILITY_REASONS.join(\", \")}`,\n );\n }\n if (typeof options.hint !== \"string\" || options.hint.length === 0) {\n throw new TypeError(\"DataAvailabilityError: hint is required and must be a non-empty string\");\n }\n const message = `[${options.reason}] ${options.hint}`;\n super(message, options);\n this.reason = options.reason;\n this.hint = options.hint;\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n reason: this.reason,\n hint: this.hint,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// SchemaValidationError\n// ---------------------------------------------------------------------------\n\nexport interface SchemaValidationErrorOptions extends TradewindsErrorOptions {\n schemaId: string;\n violations?: Array<Record<string, unknown>>;\n quarantineCount?: number;\n sampleViolations?: Array<Record<string, unknown>>;\n}\n\nexport class SchemaValidationError extends TradewindsError {\n static override defaultErrorCode = \"SCHEMA_VALIDATION_FAILED\";\n\n readonly schemaId: string;\n readonly violations: Array<Record<string, unknown>>;\n readonly quarantineCount: number;\n readonly sampleViolations: Array<Record<string, unknown>>;\n\n constructor(message: string, options: SchemaValidationErrorOptions) {\n super(message, options);\n this.schemaId = options.schemaId;\n this.violations = [...(options.violations ?? [])];\n this.quarantineCount = options.quarantineCount ?? 0;\n this.sampleViolations = [...(options.sampleViolations ?? [])];\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n schema_id: this.schemaId,\n violations: this.violations,\n quarantine_count: this.quarantineCount,\n sample_violations: this.sampleViolations,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// SourceMismatchError\n// ---------------------------------------------------------------------------\n\nexport type SourceMismatchRole = \"observations\" | \"forecasts\" | \"settlement\";\n\nexport interface SourceMismatchErrorOptions extends TradewindsErrorOptions {\n schemaSource: string;\n dataSource: string;\n role?: SourceMismatchRole | null;\n catalogWarning?: string | null;\n}\n\nexport class SourceMismatchError extends TradewindsError {\n static override defaultErrorCode = \"SOURCE_MISMATCH\";\n\n /** Canonical role-name vocabulary (design.md §R). */\n static readonly VALID_ROLES: ReadonlySet<SourceMismatchRole> = new Set([\n \"observations\",\n \"forecasts\",\n \"settlement\",\n ]);\n\n readonly schemaSource: string;\n readonly dataSource: string;\n readonly role: SourceMismatchRole | null;\n readonly catalogWarning: string | null;\n\n constructor(message: string, options: SourceMismatchErrorOptions) {\n super(message, options);\n this.schemaSource = options.schemaSource;\n this.dataSource = options.dataSource;\n this.role = options.role ?? null;\n this.catalogWarning = options.catalogWarning ?? null;\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n schema_source: this.schemaSource,\n data_source: this.dataSource,\n role: this.role,\n catalog_warning: this.catalogWarning,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// LeakageError\n// ---------------------------------------------------------------------------\n\nexport interface LeakageErrorOptions extends TradewindsErrorOptions {\n asOf: string;\n violatingCount: number;\n sampleViolations?: Array<Record<string, unknown>>;\n}\n\nexport class LeakageError extends TradewindsError {\n static override defaultErrorCode = \"LEAKAGE_DETECTED\";\n\n readonly asOf: string;\n readonly violatingCount: number;\n readonly sampleViolations: Array<Record<string, unknown>>;\n\n constructor(message: string, options: LeakageErrorOptions) {\n super(message, options);\n this.asOf = options.asOf;\n this.violatingCount = options.violatingCount;\n this.sampleViolations = [...(options.sampleViolations ?? [])];\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n as_of: this.asOf,\n violating_count: this.violatingCount,\n sample_violations: this.sampleViolations,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// TemporalDriftError\n// ---------------------------------------------------------------------------\n\nexport interface TemporalDriftErrorOptions extends TradewindsErrorOptions {\n schemaId: string;\n assertedRange: [string, string];\n violatingRows: number;\n sampleViolations?: Array<Record<string, unknown>>;\n}\n\nexport class TemporalDriftError extends TradewindsError {\n static override defaultErrorCode = \"TEMPORAL_DRIFT\";\n\n readonly schemaId: string;\n readonly assertedRange: [string, string];\n readonly violatingRows: number;\n readonly sampleViolations: Array<Record<string, unknown>>;\n\n constructor(message: string, options: TemporalDriftErrorOptions) {\n super(message, options);\n this.schemaId = options.schemaId;\n this.assertedRange = [options.assertedRange[0], options.assertedRange[1]];\n this.violatingRows = options.violatingRows;\n this.sampleViolations = [...(options.sampleViolations ?? [])];\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n schema_id: this.schemaId,\n asserted_range: [this.assertedRange[0], this.assertedRange[1]],\n violating_rows: this.violatingRows,\n sample_violations: this.sampleViolations,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// PayloadTooLargeError\n// ---------------------------------------------------------------------------\n\nexport interface PayloadTooLargeErrorOptions extends TradewindsErrorOptions {\n declaredSize: number;\n limit: number;\n acceptedModes?: string[];\n}\n\nexport class PayloadTooLargeError extends TradewindsError {\n static override defaultErrorCode = \"PAYLOAD_TOO_LARGE\";\n\n readonly declaredSize: number;\n readonly limit: number;\n readonly acceptedModes: string[];\n\n constructor(message: string, options: PayloadTooLargeErrorOptions) {\n super(message, options);\n this.declaredSize = options.declaredSize;\n this.limit = options.limit;\n this.acceptedModes = [...(options.acceptedModes ?? [])];\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n declared_size: this.declaredSize,\n limit: this.limit,\n accepted_modes: this.acceptedModes,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// DeferredMarketError (TS-W6 placeholder; mirrors mostlyright.international)\n// ---------------------------------------------------------------------------\n\nexport class DeferredMarketError extends TradewindsError {\n static override defaultErrorCode = \"DEFERRED_MARKET\";\n}\n\n// ---------------------------------------------------------------------------\n// PolymarketEventError (TS-W5 placeholder; mirrors mostlyright.markets.polymarket)\n// ---------------------------------------------------------------------------\n\nexport class PolymarketEventError extends TradewindsError {\n static override defaultErrorCode = \"POLYMARKET_EVENT_INVALID\";\n}\n\n// ---------------------------------------------------------------------------\n// HTTP-layer hierarchy (mirrors mostlyright._internal.exceptions)\n// ---------------------------------------------------------------------------\n\nexport interface TherminalErrorOptions extends TradewindsErrorOptions {\n statusCode?: number | null;\n retryAfter?: number | null;\n}\n\n/**\n * Base HTTP-layer marker. Subclass of `TradewindsError` so callers that\n * catch `TradewindsError` also catch transport errors.\n */\nexport class TherminalError extends TradewindsError {\n static override defaultErrorCode = \"HTTP_ERROR\";\n\n readonly statusCode: number | null;\n\n constructor(message: string, options: TherminalErrorOptions = {}) {\n super(message, options);\n this.statusCode = options.statusCode ?? null;\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n status_code: this.statusCode,\n };\n }\n}\n\nexport class NotFoundError extends TherminalError {\n static override defaultErrorCode = \"HTTP_NOT_FOUND\";\n\n constructor(message = \"Resource not found\", options: TherminalErrorOptions = {}) {\n super(message, { ...options, statusCode: options.statusCode ?? 404 });\n }\n}\n\nexport interface RateLimitErrorOptions extends TherminalErrorOptions {\n retryAfter?: number | null;\n}\n\nexport class RateLimitError extends TherminalError {\n static override defaultErrorCode = \"HTTP_RATE_LIMITED\";\n\n readonly retryAfter: number | null;\n\n constructor(retryAfter: number | null = 1, options: RateLimitErrorOptions = {}) {\n const msg = `Rate limit exceeded. Retry after ${retryAfter ?? \"?\"}s`;\n super(msg, { ...options, statusCode: options.statusCode ?? 429 });\n this.retryAfter = retryAfter;\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n retry_after: this.retryAfter,\n };\n }\n}\n\nexport class ValidationError extends TherminalError {\n static override defaultErrorCode = \"HTTP_BAD_REQUEST\";\n\n constructor(message = \"Invalid request\", options: TherminalErrorOptions = {}) {\n super(message, { ...options, statusCode: options.statusCode ?? 400 });\n }\n}\n\nexport class AuthenticationError extends TherminalError {\n static override defaultErrorCode = \"HTTP_UNAUTHORIZED\";\n\n constructor(message = \"Authentication required\", options: TherminalErrorOptions = {}) {\n super(message, { ...options, statusCode: options.statusCode ?? 401 });\n }\n}\n\nexport class ForbiddenError extends TherminalError {\n static override defaultErrorCode = \"HTTP_FORBIDDEN\";\n\n constructor(message = \"Access denied\", options: TherminalErrorOptions = {}) {\n super(message, { ...options, statusCode: options.statusCode ?? 403 });\n }\n}\n\nexport class ServerError extends TherminalError {\n static override defaultErrorCode = \"HTTP_SERVER_ERROR\";\n\n constructor(message = \"Server error\", options: TherminalErrorOptions = {}) {\n super(message, { ...options, statusCode: options.statusCode ?? 500 });\n }\n}\n\n// ---------------------------------------------------------------------------\n// LiveStreamError + NoLiveDataError (Phase 11)\n// ---------------------------------------------------------------------------\n\n/**\n * Base class for `mostlyright.live.stream` / `live.latest` failures.\n *\n * Mirrors Python `LiveStreamError`. Live-streaming errors are a separate\n * sub-tree from `SourceUnavailableError` because the recovery path differs —\n * `stream()` swallows empty-tick errors and waits for the next polite-floor\n * cycle. Only `latest()` raises `NoLiveDataError` on empty responses.\n */\nexport class LiveStreamError extends TradewindsError {\n static override defaultErrorCode = \"LIVE_STREAM_ERROR\";\n}\n\nexport interface NoLiveDataErrorOptions extends TradewindsErrorOptions {\n station: string;\n source: string;\n}\n\n/**\n * `mostlyright.live.latest` returned no observations for the station.\n *\n * Carries the resolved ICAO `station` and the canonical source identity\n * tag (`\"awc.live\"` / `\"iem.live\"`) so caller logs can branch by source\n * without re-parsing the message.\n */\nexport class NoLiveDataError extends LiveStreamError {\n static override defaultErrorCode = \"NO_LIVE_DATA\";\n\n readonly station: string;\n\n constructor(message: string, options: NoLiveDataErrorOptions) {\n super(message, { ...options, source: options.source });\n this.station = options.station;\n }\n\n protected override payload(): Record<string, unknown> {\n return {\n ...super.payload(),\n station: this.station,\n };\n }\n}\n","// KnowledgeView — temporal filtering by `knowledge_time`.\n//\n// Mirrors `packages/core/src/mostlyright/core/temporal/knowledge_view.py`.\n// A plain class (not an iterator subclass, not a DataFrame accessor) that\n// filters its `rows()` output to entries where `knowledge_time <= asOf`.\n//\n// Construction validates the shape of EVERY row up-front: each\n// `knowledge_time` must be a string that TimePoint accepts (rejecting naive\n// / date-only / NaN inputs). Any failure throws SchemaValidationError with\n// the violating row indices in `violations`.\n//\n// Iter-1 H2: violation `rule` vocabulary matches Python EXACTLY for MCP\n// cross-language serialization parity:\n//\n// - \"required\" — missing column / non-string knowledge_time\n// - \"tz_aware_utc\" — naive or otherwise invalid tz-aware ISO string\n//\n// See docs/design.md §M for the leakage-free training-view semantics.\n\nimport { SchemaValidationError } from \"../exceptions/index.js\";\nimport { TimePoint } from \"./timepoint.js\";\n\n/**\n * A filtered, knowledge-time-bounded view over an array of rows.\n *\n * @template Row — caller's row type. Must have a string `knowledge_time` field.\n */\nexport class KnowledgeView<Row extends { knowledge_time: string }> {\n readonly #rows: ReadonlyArray<Row>;\n readonly #asOf: TimePoint;\n // Iter-11 C13: cache the asOf epoch-µs (BigInt) for filter comparisons.\n // Previously cached `asOfMs` (epoch-ms) and compared via `Date.parse`,\n // which collapsed `.123456Z` and `.123789Z` to the same instant — a\n // µs-resolution row \"known\" after asOf could slip through.\n readonly #asOfMicros: bigint;\n\n constructor(rows: ReadonlyArray<Row>, asOf: TimePoint) {\n if (!(asOf instanceof TimePoint)) {\n throw new TypeError(`asOf must be a TimePoint; got ${typeof asOf}`);\n }\n // Eager per-row validation: every row's knowledge_time must parse as a\n // valid tz-aware ISO datetime. We piggyback on TimePoint's rejection\n // logic so the error vocabulary matches construction-time errors.\n const violations: Array<Record<string, unknown>> = [];\n for (let i = 0; i < rows.length; i++) {\n const r = rows[i];\n if (r == null || typeof r.knowledge_time !== \"string\") {\n // H2: Python `knowledge_view.py` uses `rule=\"required\"` when the\n // knowledge_time column is missing or wrong-typed. Mirror the\n // vocabulary so MCP wire payloads match cross-language.\n violations.push({\n column: \"knowledge_time\",\n row_idx: i,\n rule: \"required\",\n });\n continue;\n }\n try {\n new TimePoint(r.knowledge_time);\n } catch (e) {\n // H2: Python uses `rule=\"tz_aware_utc\"` for naive / invalid\n // datetime strings (the upstream Python check is on the column\n // dtype, not per-row, but the rule string is what the wire\n // payload pins). Preserve the parsed error message in `message`\n // for debugging.\n violations.push({\n column: \"knowledge_time\",\n row_idx: i,\n rule: \"tz_aware_utc\",\n message: String(e),\n });\n }\n }\n if (violations.length > 0) {\n throw new SchemaValidationError(\n `KnowledgeView received ${violations.length} row(s) with invalid knowledge_time`,\n { schemaId: \"<runtime>\", violations },\n );\n }\n this.#rows = rows;\n this.#asOf = asOf;\n this.#asOfMicros = asOf.toEpochMicros();\n }\n\n /**\n * Return a freshly filtered array — only rows where\n * `knowledge_time <= asOf`. The returned array is a NEW reference each\n * call (defensive copy semantics), so callers can mutate it without\n * affecting subsequent calls.\n */\n rows(): ReadonlyArray<Row> {\n // Iter-11 C13: per-row comparison goes through TimePoint so we get\n // epoch-µs precision. Constructing a TimePoint per row is more work\n // than the previous `Date.parse(...)` shortcut, but the constructor\n // is also the only path that captures 4-6-digit fractional seconds\n // correctly — without it, two distinct µs-resolution rows would\n // compare equal via `Date.parse` (which is ms-only).\n return this.#rows.filter((r) => {\n const ktMicros = new TimePoint(r.knowledge_time).toEpochMicros();\n return ktMicros <= this.#asOfMicros;\n });\n }\n\n /** The as-of cutoff supplied at construction. */\n get asOf(): TimePoint {\n return this.#asOf;\n }\n}\n","// assertNoLeakage + LeakageDetector — loud assertion of as-of leakage absence.\n//\n// Mirrors `packages/core/src/mostlyright/core/temporal/leakage.py`. Where\n// KnowledgeView silently filters, assertNoLeakage throws LeakageError if\n// any row has knowledge_time > asOf. The error payload follows design.md\n// §D: violatingCount + sampleViolations (capped at 10).\n//\n// Behavior parity with Python `leakage.py`:\n// - Missing `knowledge_time` field → SchemaValidationError with\n// `violations: [{ column: \"knowledge_time\", rule: \"required\" }]`.\n// Matches Python's `\"knowledge_time\" not in df.columns` branch.\n// - Non-string `knowledge_time` value → SchemaValidationError with\n// `rule: \"datetime_dtype\"`. Matches Python's\n// `is_datetime64_any_dtype(col)` check.\n// - Naive / tz-less / unparseable `knowledge_time` string →\n// SchemaValidationError with `rule: \"tz_aware_utc\"`. Matches Python's\n// `col.dt.tz is None` check.\n// - Iter-3 C9 fix: previously these three cases were SKIPPED silently\n// (rows quietly dropped from the `>` comparison), which let malformed\n// temporal data pass the leakage gate. The Python contract raises;\n// the TS port now matches.\n// - Sample cap = 10.\n// - Wire format for `as_of` AND `sample_violations[].knowledge_time` uses\n// the Python `datetime.isoformat()` shape (`\"...T12:00:00+00:00\"`) via\n// `TimePoint.toPythonIso()` — iter-1 H1 fix. MCP clients comparing\n// these strings across Python and TS see byte-equivalent values.\n\nimport { LeakageError, SchemaValidationError } from \"../exceptions/index.js\";\nimport { TimePoint } from \"./timepoint.js\";\n\nconst SAMPLE_CAP = 10;\nconst RUNTIME_SCHEMA_ID = \"<runtime>\";\n\n/**\n * Throw {@link LeakageError} if any row's `knowledge_time` is strictly\n * greater than `asOf`. Leak-free input returns void.\n *\n * Loudly rejects rows whose `knowledge_time` is missing, not a string,\n * or not a tz-aware ISO 8601 datetime — these surface as\n * {@link SchemaValidationError} (rule: `required` /\n * `datetime_dtype` / `tz_aware_utc`), mirroring Python's\n * `assert_no_leakage` validation contract.\n *\n * @template Row — caller's row type. Must have a string `knowledge_time` field.\n */\nexport function assertNoLeakage<Row extends { knowledge_time: string }>(\n rows: ReadonlyArray<Row>,\n asOf: TimePoint,\n): void {\n if (!(asOf instanceof TimePoint)) {\n throw new TypeError(`asOf must be a TimePoint; got ${typeof asOf}`);\n }\n // Iter-11 C13: compare via TimePoint.after() (epoch-µs) rather than\n // `toUTCDate().getTime()` (epoch-ms). A row \"known\" 333µs after `asOf`\n // would silently pass the ms-comparison gate; the µs-aware comparison\n // catches it.\n const violations: Array<{ row_idx: number; knowledge_time: string }> = [];\n for (let i = 0; i < rows.length; i++) {\n const r = rows[i];\n // Reject missing rows / missing knowledge_time field (iter-3 C9).\n // Python: `\"knowledge_time\" not in df.columns` →\n // SchemaValidationError(rule=\"required\", column=\"knowledge_time\")\n if (r == null) {\n throw new SchemaValidationError(\"assertNoLeakage requires 'knowledge_time' on every row\", {\n schemaId: RUNTIME_SCHEMA_ID,\n violations: [{ column: \"knowledge_time\", rule: \"required\", row_idx: i }],\n quarantineCount: rows.length,\n sampleViolations: [{ column: \"knowledge_time\", rule: \"required\", row_idx: i }],\n });\n }\n const rec = r as unknown as Record<string, unknown>;\n if (!(\"knowledge_time\" in rec) || rec.knowledge_time === undefined) {\n throw new SchemaValidationError(\"assertNoLeakage requires 'knowledge_time' on every row\", {\n schemaId: RUNTIME_SCHEMA_ID,\n violations: [{ column: \"knowledge_time\", rule: \"required\", row_idx: i }],\n quarantineCount: rows.length,\n sampleViolations: [{ column: \"knowledge_time\", rule: \"required\", row_idx: i }],\n });\n }\n // Reject non-string knowledge_time (iter-3 C9). Python: a non-datetime\n // column raises with `rule=\"datetime_dtype\"`. The TS analog is a\n // per-row value that isn't even a string.\n const raw = rec.knowledge_time;\n if (typeof raw !== \"string\") {\n throw new SchemaValidationError(\n `assertNoLeakage requires string knowledge_time; got ${typeof raw} at row ${i}`,\n {\n schemaId: RUNTIME_SCHEMA_ID,\n violations: [{ column: \"knowledge_time\", rule: \"datetime_dtype\", row_idx: i }],\n quarantineCount: rows.length,\n sampleViolations: [{ column: \"knowledge_time\", rule: \"datetime_dtype\", row_idx: i }],\n },\n );\n }\n // Reject naive / unparseable / non-tz-aware strings (iter-3 C9).\n // Python: `col.dt.tz is None` raises with `rule=\"tz_aware_utc\"`.\n // The TS analog: TimePoint's constructor rejects anything that isn't\n // a tz-aware ISO 8601 datetime (date-only, naive, unparseable, etc.).\n // Re-wrap any TimePoint constructor failure as SchemaValidationError\n // with the Python vocab so cross-SDK MCP consumers see the same\n // rule strings.\n let kt: TimePoint;\n try {\n kt = new TimePoint(raw);\n } catch (_err) {\n throw new SchemaValidationError(\n `assertNoLeakage requires tz-aware UTC knowledge_time; got ${JSON.stringify(raw)} at row ${i}`,\n {\n schemaId: RUNTIME_SCHEMA_ID,\n violations: [{ column: \"knowledge_time\", rule: \"tz_aware_utc\", row_idx: i }],\n quarantineCount: rows.length,\n sampleViolations: [{ column: \"knowledge_time\", rule: \"tz_aware_utc\", row_idx: i }],\n },\n );\n }\n // Iter-11 C13: µs-aware comparison. `kt.after(asOf)` uses epoch-µs\n // (BigInt), so a `.123456Z` knowledge_time vs `.123000Z` asOf is\n // correctly flagged as leakage (456µs strictly after asOf), whereas\n // the previous `.getTime() > asOfMs` (epoch-ms) would have missed it.\n if (kt.after(asOf)) {\n // Re-emit knowledge_time in the Python isoformat shape so cross-\n // language MCP consumers see byte-equivalent strings.\n violations.push({ row_idx: i, knowledge_time: kt.toPythonIso() });\n }\n }\n if (violations.length === 0) return;\n throw new LeakageError(`Found ${violations.length} row(s) with knowledge_time > asOf`, {\n asOf: asOf.toPythonIso(),\n violatingCount: violations.length,\n sampleViolations: violations.slice(0, SAMPLE_CAP),\n });\n}\n\n/**\n * Convenience wrapper for repeated leakage checks against a fixed `asOf`.\n *\n * ```ts\n * const detector = new LeakageDetector(asOf);\n * detector.check(trainingRows);\n * detector.check(featureRows);\n * ```\n */\nexport class LeakageDetector {\n readonly #asOf: TimePoint;\n\n constructor(asOf: TimePoint) {\n if (!(asOf instanceof TimePoint)) {\n throw new TypeError(`asOf must be a TimePoint; got ${typeof asOf}`);\n }\n this.#asOf = asOf;\n }\n\n get asOf(): TimePoint {\n return this.#asOf;\n }\n\n check<Row extends { knowledge_time: string }>(rows: ReadonlyArray<Row>): void {\n assertNoLeakage(rows, this.#asOf);\n }\n}\n"],"mappings":"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","names":[]}
|