File size: 2,966 Bytes
518343a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
 * 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)');
  }
}