File size: 25,310 Bytes
0fff343
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
"use client";

import React from "react";
import GenerationsLoopDiagram from "./diagrams/GenerationsLoopDiagram";
import LambdaTradeoffDiagram from "./diagrams/LambdaTradeoffDiagram";
import MaxSetsDiagram from "./diagrams/MaxSetsDiagram";
import PermutationsHistogramDiagram from "./diagrams/PermutationsHistogramDiagram";
import PrefilterFunnelDiagram from "./diagrams/PrefilterFunnelDiagram";
import SeedDiceDiagram from "./diagrams/SeedDiceDiagram";

export type ParamKey =
  | "generations"
  | "population"
  | "genes_per_set"
  | "max_sets"
  | "lambda"
  | "seed"
  | "permutations"
  | "prefilter_n"
  // Objective help keys reuse the same modal infrastructure.
  | "obj_msi"
  | "obj_tmb"
  | "obj_hpv"
  | "obj_unsupervised"
  // Misc rich helpers that don't fit the hover tooltip envelope
  // (multi-paragraph, table-cell-anchored).
  | "module_survival";

export interface ParamHelpEntry {
  title: string;
  short: string;
  detailed: React.ReactNode;
}

// ---------- shared bits --------------------------------------------------

function Item({
  label,
  children,
}: {
  label: string;
  children: React.ReactNode;
}) {
  return (
    <p className="mb-3 last:mb-0 text-sm leading-relaxed text-ink">
      <strong className="text-ink">{label}:</strong>{" "}
      <span>{children}</span>
    </p>
  );
}

function Caption({ children }: { children: React.ReactNode }) {
  return (
    <p className="mb-3 text-xs italic text-muted">{children}</p>
  );
}

function Mono({ children }: { children: React.ReactNode }) {
  return (
    <code className="font-mono text-[12.5px] text-ink">{children}</code>
  );
}

function ExampleList({
  rows,
}: {
  rows: React.ReactNode[];
}) {
  return (
    <ul className="my-2 ml-5 list-disc text-sm leading-relaxed text-ink marker:text-muted">
      {rows.map((r, i) => (
        <li key={i}>{r}</li>
      ))}
    </ul>
  );
}

// ---------- the registry -------------------------------------------------

export const PARAM_HELP: Record<ParamKey, ParamHelpEntry> = {
  generations: {
    title: "Generations",
    short:
      "How many rounds of 'keep the best, breed, mutate' the engine runs. More rounds refine further, but take longer.",
    detailed: (
      <>
        <Item label="What it is">
          one &ldquo;generation&rdquo; is a single round of the engine&rsquo;s
          loop β€” score every program, keep the best, then breed and mutate them
          into the next batch. Generations is how many rounds it runs.
        </Item>
        <Item label="How it works">
          the engine starts with random programs (mostly poor) and each round
          nudges the population toward better ones β€” like selective breeding.
        </Item>
        <Item label="More vs fewer">
          more rounds refine the winners further, but with diminishing returns
          once it has converged. The harder part is usually <em>discovering</em>{" "}
          the right genes in the first place (driven by Population and
          mutation), not polishing β€” so extra generations help less once the
          curve flattens.
        </Item>
        <Item label="Typical">30–60.</Item>
        <GenerationsLoopDiagram />
        <Caption>
          A 4-step loop: random programs β†’ score each β†’ keep the best β†’ breed
          &amp; mutate β†’ (back to score). The loop arrow is labelled
          &ldquo;Γ— Generations&rdquo;.
        </Caption>
      </>
    ),
  },

  population: {
    title: "Population",
    short:
      "How many candidate programs compete in each round. A 'program' is a small pipeline built by composing the DSL operators (Select β†’ Reduce β†’ Fit) over the gene data. More programs explores more options, but is slower.",
    detailed: (
      <>
        <Item label="What it is">
          how many candidate programs compete each round. A &ldquo;program&rdquo;
          is a small pipeline built from the DSL operators β€”{" "}
          <strong>Select</strong> (pick genes) β†’ <strong>Reduce</strong>{" "}
          (average them into one score) β†’ <strong>Fit</strong> (use the score
          to separate the groups).
        </Item>
        <Item label="Why it matters">
          a bigger population samples more genes and structures each round, so
          it&rsquo;s the main lever for <em>discovery</em> β€” finding the right
          genes at all. Generations then refine what was found.
        </Item>
        <p className="mb-1 text-sm font-semibold text-ink">
          Example β€” three random programs in a population:
        </p>
        <ExampleList
          rows={[
            <Mono key="a">average(g04823, g11201, g00917) β†’ score</Mono>,
            <Mono key="b">
              average(g15522, g02013) βˆ’ average(g08840, g00231) β†’ score
            </Mono>,
            <Mono key="c">average(g07788, g13002, g05340) β†’ score</Mono>,
          ]}
        />
        <Item label="More vs fewer">
          more explores more options, but each round is slower.
        </Item>
        <Item label="Typical">150–300.</Item>
      </>
    ),
  },

  genes_per_set: {
    title: "Genes / set",
    short:
      "The most genes a single score may use. Smaller keeps programs simple and readable.",
    detailed: (
      <>
        <Item label="What it is">
          a &ldquo;set&rdquo; is the group of genes averaged together into a
          single score. Genes/set is the maximum size of that group.
        </Item>
        <Item label="Example">
          <Mono>
            Select(g05347, g00048, g06271) β†’ average β†’ one score
          </Mono>{" "}
          (uses 3; the cap might be 8).
        </Item>
        <Item label="Smaller vs larger">
          smaller keeps each score simple and readable; larger lets one score
          blend more genes.
        </Item>
        <Item label="Pairs with Max sets">
          Genes/set is the <em>width</em> of each group; Max sets is{" "}
          <em>how many</em> groups.
        </Item>
      </>
    ),
  },

  max_sets: {
    title: "Max sets",
    short:
      "How many separate gene-scores a program may combine (1 or 2). 2 lets it build a small 'score made of scores'.",
    detailed: (
      <>
        <Item label="What it is">
          how many <em>separate</em> gene groups a program may build and then
          combine (1 or 2). They use different genes β€” not the same ones
          reused.
        </Item>
        <p className="mb-1 text-sm font-semibold text-ink">Example (2 sets):</p>
        <ExampleList
          rows={[
            <>
              Set 1:{" "}
              <Mono>average(g05347, g00048, g06271) β†’ scoreA</Mono>
            </>,
            <>
              Set 2: <Mono>average(g15522, g02013) β†’ scoreB</Mono>
            </>,
            <>
              Combine: <Mono>scoreA βˆ’ scoreB β†’ final score</Mono>
            </>,
          ]}
        />
        <Item label="Why two">
          to capture a <em>contrast</em> β€” e.g. one group that&rsquo;s low in
          MSI-H and one that&rsquo;s high; the gap between them can separate
          better than either alone (like &ldquo;repair activity minus immune
          activity&rdquo;). One set can&rsquo;t express that; two can. If one
          group is enough, the engine can still use 1.
        </Item>
        <MaxSetsDiagram />
        <Caption>
          Two short chains feed a Combine(sub) node and produce a final score.
        </Caption>
      </>
    ),
  },

  lambda: {
    title: "Ξ» (size penalty)",
    short:
      "How hard the engine is penalised for using more genes. Higher pushes it toward fewer genes (simpler answers).",
    detailed: (
      <>
        <Item label="What it is">
          a &ldquo;price per gene&rdquo; that discourages bloated programs.
          Every program is graded on a single number:
        </Item>
        <p className="mb-3 ml-5 text-sm">
          <Mono>fitness = separation βˆ’ Ξ» Γ— (number of genes)</Mono>
        </p>
        <p className="mb-3 text-sm leading-relaxed text-ink">
          so the engine ranks by accuracy <em>minus</em> a size tax.
        </p>
        <Item label="The key idea">
          Ξ» is the admission price each gene must beat: with Ξ» = 0.005, a gene
          is only worth keeping if it adds more than 0.005 of separation.
        </Item>
        <Item label="Worked example">
          a 3-gene program scores 0.90 β†’ net 0.90 βˆ’ 0.015 = 0.885. Add a 4th
          gene that lifts it to 0.903 (only +0.003) β†’ net 0.883, <em>lower</em>,
          so it&rsquo;s rejected. A 4th gene that lifts it to 0.91 (+0.01) β†’
          net 0.890, <em>higher</em>, so it&rsquo;s kept.
        </Item>
        <Item label="Turning the knob">
          Ξ» = 0 β†’ genes free β†’ bloated, overfit programs. Ξ» small (0.005) β†’
          trims useless genes, keeps useful ones. Ξ» large β†’ very lean programs,
          but may drop useful genes.
        </Item>
        <LambdaTradeoffDiagram />
        <Caption>
          Raw separation rises and plateaus; net fitness peaks at an
          intermediate size, and a larger Ξ» shifts the peak to fewer genes.
        </Caption>
      </>
    ),
  },

  seed: {
    title: "Seed",
    short:
      "The starting point for the engine's randomness. The same seed reproduces the exact same run; change it to see a different run.",
    detailed: (
      <>
        <Item label="Why randomness">
          the space of possible programs is astronomically large (picking even
          8 genes out of 20,000 is ~10²⁹ combinations), so the engine
          can&rsquo;t try them all β€” it explores with randomness: random
          starting programs, random mutations, random breeding.
        </Item>
        <Item label="What the seed is">
          computers don&rsquo;t make true randomness; a formula generates a
          sequence of numbers, each from the previous one. The seed is the{" "}
          <em>starting number</em> fed into that formula β€” everything random
          flows from it. (One common formula:{" "}
          <Mono>next = (1664525 Γ— current + 1013904223) mod 2Β³Β²</Mono>; with
          seed 42 the first value is 1,083,814,273, then mapped onto a gene
          position.)
        </Item>
        <Item label="What it does">
          think of the seed as one fixed list of dice rolls used in order
          across the whole run β€” early rolls pick the starting genes, later
          rolls drive mutations. Same seed β†’ same list β†’ identical run.
          Different seed β†’ a different list β†’ a different run.
        </Item>
        <Item label="Why vary it">
          because the search has luck in it, run a few seeds (42, 7, 123) and
          see whether the same genes keep appearing β€” that tells you a finding
          is real, not a fluke.
        </Item>
        <SeedDiceDiagram />
        <Caption>
          The seed is one fixed list of dice rolls; early rolls pick initial
          genes, later rolls drive mutations.
        </Caption>
      </>
    ),
  },

  permutations: {
    title: "Permutations",
    short:
      "How many times we re-run on deliberately scrambled labels to check the result isn't luck. The real result must beat these chance runs (that's the 'permutation p').",
    detailed: (
      <>
        <Item label="What it checks">
          whether the winning score is real signal or could have come up by
          luck. The engine searches so hard that some programs separate the
          groups well by pure coincidence, so a high score alone isn&rsquo;t
          proof.
        </Item>
        <Item label="How it works">
          shuffle the MSI-H / MSS labels across patients at random β€” this
          breaks any real gene↔label link, creating a &ldquo;no-signal
          world&rdquo;. Re-score there: any score above 0.5 is pure chance.
          Repeat many times (this knob = how many) to build a picture of what
          luck looks like.
        </Item>
        <Item label="Reading it">
          permutation p = the fraction of shuffled runs that scored β‰₯ the real
          winner. p = 0.005 means only 0.5% of chance runs matched it β†’ very
          unlikely a fluke. Conventionally p &lt; 0.05 is the &ldquo;unlikely
          to be luck&rdquo; line.
        </Item>
        <Item label="The knob">
          with 200 shuffles the smallest p you can report is about 1 / 200 β‰ˆ
          0.005; more permutations give a finer, more trustworthy p.
        </Item>
        <PermutationsHistogramDiagram />
        <Caption>
          200 shuffled-label runs cluster near 0.58; the real result at 0.89
          sits past every one of them, giving p β‰ˆ 0.005.
        </Caption>
      </>
    ),
  },

  prefilter_n: {
    title: "Prefilter top-N",
    short:
      "By default the engine searches all ~20,000 genes, so nothing is pre-excluded. Turning this on narrows to the N most promising genes first β€” faster, but it can drop a real gene that only shows signal in combination.",
    detailed: (
      <>
        <Item label="Default (off)">
          the engine searches all ~20,000 genes, so nothing is pre-excluded β€”
          the most honest setting for a discovery demo.
        </Item>
        <Item label="Turning it on">
          first narrows to the N genes most individually associated with the
          target (computed name-blind, on the training split only), then
          searches within that shortlist. Faster, because the space is ~10Γ—
          smaller.
        </Item>
        <Item label="The trade-off">
          speed and focus vs completeness. A univariate shortlist can drop a
          gene that only matters <em>in combination</em> (no signal on its
          own), and it sets a ceiling β€” if a gene isn&rsquo;t in the
          shortlist, the engine can never find it.
        </Item>
        <PrefilterFunnelDiagram />
        <Caption>
          ~20,000 genes β†’ keep the N most individually associated β†’ the GP
          searches within N. Off by default = no funnel.
        </Caption>
      </>
    ),
  },

  // ---------- Objective explainers ----------

  obj_msi: {
    title: "MSI separation",
    short:
      "Reward programs whose score separates MSI-H from MSS (measured by AUROC). It rewards ANY separator, so it often finds shortcut genes, not the cause.",
    detailed: (
      <>
        <ObjectiveIntro />
        <Item label="What it optimises">
          a score that ranks MSI-H patients above MSS β€” i.e. tells the two
          subtypes apart.
        </Item>
        <Item label="How it&rsquo;s scored (AUROC)">
          pick one random MSI-H and one random MSS patient; AUROC is the
          chance the score puts the MSI-H one higher. 0.5 = coin-flip (no
          separation), 1.0 = perfect. We use AUROC, not plain accuracy,
          because MSI-H is only ~15% of patients β€” &ldquo;always guess
          MSS&rdquo; would look 85% accurate while separating nothing.
        </Item>
        <Item label="Represented as">
          <code className="font-mono text-ink">{`{ target: msi, metric: AUROC }`}</code>
          {" "}β€” the engine sees only the score + the MSI label, never
          gene names.
        </Item>
        <Item label="The catch">
          it rewards ANY separator. MSI-H and MSS differ in thousands of
          genes, so it usually grabs easy &ldquo;shortcut&rdquo; genes
          (consequences or coincidences), not the causal MMR genes.
        </Item>
        <ObjectiveFooter />
      </>
    ),
  },

  obj_tmb: {
    title: "Mutation burden",
    short:
      "Reward programs whose score is negatively associated with mutation burden (low score ↔ high TMB) β€” the broken spell-checker's fingerprint. A sharper proxy for the cause, but still association, not proof.",
    detailed: (
      <>
        <ObjectiveIntro />
        <Item label="What it optimises">
          a score that goes DOWN as mutation count goes UP β€” the
          fingerprint of a broken repair gene (switch it off β†’ mutations
          pile up). The repair genes are the MMR set: MLH1, MSH2, MSH6,
          PMS2.
        </Item>
        <Item label="Represented as">
          <code className="font-mono text-ink">{`{ target: tmb, metric: correlation, direction: negative }`}</code>
          {" "}β€” the engine sees only the score + the TMB numbers.
        </Item>
        <Item label="Why &lsquo;negative&rsquo; (not just &lsquo;related&rsquo;)">
          we reward the score being NEGATIVELY correlated with TMB.
          Rewarding any correlation would also pick genes that RISE with
          mutations β€” the opposite of the repair signature.
        </Item>
        <Item label="Correlation isn&rsquo;t causation (honest)">
          this is a mechanism-shaped association β€” a much better proxy
          for the cause than predicting the label, but it doesn&rsquo;t
          prove causation. Genes silenced alongside the repair genes can
          mimic the same low-expression↔high-TMB pattern.
        </Item>
        <Item label="Toward causal">
          the program can choose to ADJUST for confounders (age, stage)
          via the Effect operator β€” the most causal move the
          observational data honestly allows. The engine decides whether
          it helps; we don&rsquo;t hardcode it.
        </Item>
        <ObjectiveFooter />
      </>
    ),
  },

  obj_hpv: {
    title: "HPV detection",
    short:
      "Find a gene-expression pattern that tells HPV+ tumours apart from HPVβˆ’. Scored by AUROC (0.5 = coin-flip, 1.0 = perfect).",
    detailed: (
      <>
        <p className="mb-4 rounded-md border border-border bg-bg p-3 text-[12.5px] leading-relaxed text-ink">
          <strong className="text-ink">HPV detection.</strong> Head &amp;
          neck cancers split into two kinds: those caused by the HPV
          virus (HPV+) and those that aren&rsquo;t (HPVβˆ’). The engine
          looks for a gene-expression pattern that tells the two apart.
        </p>
        <Item label="How it&rsquo;s scored (AUROC)">
          pick one random HPV+ and one random HPVβˆ’ patient; AUROC is
          the chance the engine&rsquo;s score puts the HPV+ one higher.
          0.5 is a coin-flip, 1.0 is perfect. Because only ~15% of
          tumours are HPV+, we use AUROC rather than plain accuracy β€”
          &ldquo;always guess HPVβˆ’&rdquo; would look 85% accurate while
          separating nothing.
        </Item>
        <Item label="What the engine sees">
          only the score its program produces and the HPV+/HPVβˆ’ label β€”
          never gene names. That&rsquo;s what makes recovering the
          known biology afterwards a real rediscovery, not a lookup.
        </Item>
        <Item label="Important">
          this is <em>detecting a known viral fingerprint</em> (the
          virus switches specific genes on), not discovering a new
          cause. The cause is the virus itself. A high AUROC means the
          engine recognised the fingerprint blind.
        </Item>
        <ObjectiveFooter />
      </>
    ),
  },

  obj_unsupervised: {
    title: "Unsupervised",
    short:
      "No target column at all. The engine searches for a program whose score splits patients into two clean groups; afterwards we check what that split lines up with.",
    detailed: (
      <>
        <ObjectiveIntro />
        <Item label="No target at all">
          Unlike the other objectives, this one is given NO target β€” only
          the gene numbers. The search never sees MSI, TMB, or any label.
        </Item>
        <Item label="What it optimises">
          a score that divides patients into two as-cleanly-separated-as-
          possible groups (measured by cluster separation). It is NOT told
          what the groups should be.
        </Item>
        <Item label="Shape vs aim">
          it always produces a two-group split, but it doesn&rsquo;t aim
          at MSS/dMMR β€” or anything. It finds whatever the strongest
          natural division in the data is.
        </Item>
        <Item label="How we read it">
          after the run we check what the discovered split lines up with
          β€” MSI? TMB? β€” the &ldquo;post-hoc alignment&rdquo; in the
          Result panel.
        </Item>
        <Item label="The win">
          if the strongest natural split turns out to BE the MSS-vs-dMMR
          divide, it aligns with MSI at high AUROC β†’ the engine
          rediscovered the subtype without ever being told it exists.
        </Item>
        <Item label="Honest expectation">
          it may instead land on a different dominant axis (e.g. immune
          hot vs cold) that only partly overlaps MSI. MSI-H is only ~15%
          of patients, so a clean recovery isn&rsquo;t guaranteed β€” the
          result is HOW MUCH the blind split overlaps MSS/dMMR.
        </Item>
        <UnsupObjectiveFooter />
      </>
    ),
  },

  module_survival: {
    title: "What β€œSurvives” checks",
    short:
      "Whether a group still separates the label when you take away a possible confounder β€” something that travels with the label but isn't its biology.",
    detailed: (
      <>
        <Item label="The question">
          whether a group still separates HPV when you take away a possible
          <em> confounder</em> β€” something that travels with HPV but
          isn&rsquo;t HPV biology. If the group&rsquo;s AUROC mostly came
          from the confounder, it collapses when the confounder is held
          constant; if the signal is real, it holds.
        </Item>
        <Item label="Site">
          HPV+ tumours are mostly in the oropharynx (back of the throat),
          so a gene could look like an &ldquo;HPV gene&rdquo; just by
          marking that location. The site check re-scores the group using
          ONLY oropharynx patients β€” everyone the same location. The two
          numbers in the chip read <em>full-cohort β†’ oropharynx-only</em>.
        </Item>
        <Item label="Purity">
          a tumour sample is a mix of cancer cells and immune cells; HPV+
          tumours carry more immune cells, so a gene could look like an
          &ldquo;HPV gene&rdquo; just by marking immune content. The
          purity check re-scores using only the &ldquo;purest&rdquo;
          (least-immune) tumours. It&rsquo;s usually <Mono>β€”</Mono> here
          because those tumours include almost no HPV+ patients, so
          there&rsquo;s nothing to test.
        </Item>
        <Item label="Reading the chips">
          <span className="font-semibold" style={{ color: "#2F6E4C" }}>
            βœ“
          </span>{" "}
          = held up when the confounder was held constant (likely real
          signal).{" "}
          <span className="font-semibold" style={{ color: "#A64242" }}>
            βœ—
          </span>{" "}
          = dropped past the tolerance (part of it was the confounder).{" "}
          <Mono>β€”</Mono> = couldn&rsquo;t test (too few patients per
          class in the subgroup).
        </Item>
        <Caption>
          Tolerance is 0.05 AUROC: a subgroup AUROC within 0.05 of the
          full-cohort AUROC counts as &ldquo;survives.&rdquo; HNSC/HPV
          coherence-on runs only β€” other (dataset, target) pairs omit
          this column.
        </Caption>
      </>
    ),
  },
};

function ObjectiveIntro() {
  return (
    <p className="mb-4 rounded-md border border-border bg-bg p-3 text-[12.5px] leading-relaxed text-ink">
      An objective is the rule that scores every program β€” the fitness the
      engine maximises. It&rsquo;s computed only from the program&rsquo;s
      per-patient output (and, for the supervised objectives, a target
      column β€” MSI, TMB, or HPV β€” never gene names). It sets what
      &ldquo;good&rdquo; means; the engine then composes DSL programs to
      satisfy it.
    </p>
  );
}

function ObjectiveFooter() {
  return (
    <p className="mt-4 border-t border-border pt-3 text-[11.5px] italic text-muted">
      What the program chooses is how to build the score and how to
      compare it β€” a raw association (Associate) or a confounder-adjusted
      one (Effect), plus the correlation kind. What stays outside the DSL
      is the compass: the target it&rsquo;s scored against, in which
      direction, judged honestly on held-out data. The program can&rsquo;t
      pick the target β€” that would let the answer into the language.
    </p>
  );
}

// Unsupervised has no target / no Associate / no Effect β€” its footer
// is the deeper invariant: the program can't see a target to optimise
// against, which is exactly what makes a downstream label-alignment
// meaningful.
function UnsupObjectiveFooter() {
  return (
    <p className="mt-4 border-t border-border pt-3 text-[11.5px] italic text-muted">
      The only thing outside the DSL here is &ldquo;find the cleanest
      split, judged honestly on held-out data&rdquo; β€” there is no target,
      and the program can&rsquo;t see one. That&rsquo;s exactly what makes
      it meaningful when the split turns out to line up with a known
      label (MSI on the colorectal cohort, HPV on head &amp; neck): the
      engine wasn&rsquo;t told to look for it.
    </p>
  );
}