a11oy / packages /sequence-pipeline /src /tabulated-statistic.ts
betterwithage's picture
sync(space): complete build context — fix BUILD_ERROR (CTO)
518343a verified
Raw
History Blame
2.97 kB
/**
* 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)');
}
}