Spaces:
Running
Running
| /** | |
| * Tabulated-statistic primitive — the canonical output shape for | |
| * `pipeline.tabulated-statistic.v1` receipts. | |
| * | |
| * The CRISPResso2 lesson re-expressed: every row carries a CI, and | |
| * absence is itself a row (`isNegativeSpace: true`), never a missing | |
| * row. The validators below are what the receipt-write boundary calls. | |
| */ | |
| export interface TabulatedRow { | |
| /** Row label (e.g. allele name, incident class). */ | |
| readonly label: string; | |
| /** Integer count, ≥ 0. */ | |
| readonly count: number; | |
| /** `count / totalTrials`, in [0, 1]. */ | |
| readonly fraction: number; | |
| /** Lower CI bound ∈ [0, 1]; CRISPResso2 lesson: mandatory. */ | |
| readonly ciLower: number; | |
| /** Upper CI bound ∈ [0, 1]; CRISPResso2 lesson: mandatory. */ | |
| readonly ciUpper: number; | |
| /** True if this row represents "no event" (e.g. unmodified reference). */ | |
| readonly isNegativeSpace: boolean; | |
| } | |
| export interface TabulatedStatistic { | |
| /** Total trials underlying the table. */ | |
| readonly totalTrials: number; | |
| /** Rows, in the order the pipeline emitted them. */ | |
| readonly rows: readonly TabulatedRow[]; | |
| /** Method reference (e.g. `'wilson-0.95'`). */ | |
| readonly methodRef: string; | |
| /** True iff the schema declares this domain has a meaningful "no event" | |
| * label and therefore requires at least one `isNegativeSpace` row. */ | |
| readonly requiresNegativeSpace: boolean; | |
| } | |
| /** | |
| * Validation for the receipt-write boundary. | |
| * | |
| * Rejects rows with `null` / non-finite CI bounds (no claim without an | |
| * interval), rejects bounds that do not surround the point estimate, | |
| * and — when `requiresNegativeSpace` is true — rejects tables that lack | |
| * an explicit negative-space row. | |
| */ | |
| export function validateTabulatedStatistic(stat: TabulatedStatistic): void { | |
| if (!Array.isArray(stat.rows) || stat.rows.length === 0) { | |
| throw new Error('tabulated-statistic: rows[] must be non-empty'); | |
| } | |
| let sawNegativeSpace = false; | |
| for (const row of stat.rows) { | |
| if (!Number.isFinite(row.ciLower) || !Number.isFinite(row.ciUpper)) { | |
| throw new Error(`tabulated-statistic: row "${row.label}" missing CI bounds (no claim without an interval)`); | |
| } | |
| if (row.ciLower < 0 || row.ciUpper > 1 || row.ciLower > row.ciUpper) { | |
| throw new Error(`tabulated-statistic: row "${row.label}" CI [${row.ciLower}, ${row.ciUpper}] is malformed`); | |
| } | |
| if (row.fraction < row.ciLower - 1e-9 || row.fraction > row.ciUpper + 1e-9) { | |
| throw new Error(`tabulated-statistic: row "${row.label}" fraction ${row.fraction} not within CI`); | |
| } | |
| if (row.count < 0 || !Number.isInteger(row.count)) { | |
| throw new Error(`tabulated-statistic: row "${row.label}" count ${row.count} must be a non-negative integer`); | |
| } | |
| if (row.isNegativeSpace) sawNegativeSpace = true; | |
| } | |
| if (stat.requiresNegativeSpace && !sawNegativeSpace) { | |
| throw new Error('tabulated-statistic: schema requires a negative-space row but none was emitted (absence is a row)'); | |
| } | |
| } | |