Skip to content

Forecasting Must Have Variables

Question answered: What data must exist for a disease/indication before the forecasting model can produce a correct forecast — for Solid Tumor and for Hematology?

Use this for data gathering. Every claim below was verified against code in both repos (bioloupe-data-gov and bioloupe-forecasting). The bioloupe-data-gov readiness gate was re-verified on 2026-06-05 — the displayName / type-value hardening (see Gate alignment) — and again on 2026-06-16, when the per-line category requirement was dropped from the gate because the forecasting client derives category structurally (see category is client-derived, not gated), and again on 2026-06-17, when the per-line displayName requirement was dropped because the client falls back to a non-blank derived line name whenever no curated row is stored, so a missing displayName can no longer blank a line — though a stored displayName stays an optional curated label (see displayName is not gated, but stays a curated label); the bioloupe-forecasting engine behavior was verified on 2026-05-28; the Tier-1 treatmentRate rule was aligned to the model on 2026-05-29. Code is the source of truth, not the prose. file:line evidence is cited throughout so it can be re-checked.


⚠️ Read this first — the gate now enforces a correct forecast

Section titled “⚠️ Read this first — the gate now enforces a correct forecast”

The server’s ReadinessChecker is the single gate deciding which diseases the forecasting tool may show and forecast. It enforces every field the engine needs for a correct forecast — not just the ones that make a disease appear:

  • the 3 common disease-level variables,
  • the type-specific stage / funnel variables, and
  • a type value that is actually recognized ("Solid Tumor" or "Hematology").

If any is missing — or the type value is unrecognized — the disease is hidden from the forecasting tool’s indication dropdown. Safe: the user never sees it. This closes the previously-documented gap where a disease could pass readiness yet silently forecast empty or structurally wrong.

category is no longer in this list. It was gate-enforced from 2026-06-05 until 2026-06-16, when it was dropped: the forecasting client derives each line’s category structurally from the line id (deriveLineCategory), so a stored category row is redundant and a missing/wrong one can no longer break a forecast. See category is client-derived, not gated.

displayName is no longer in this list either. It was gate-enforced from 2026-06-05 until 2026-06-17, when it was dropped: the forecasting client falls back to a name derived from the line id (deriveLineDisplayName) whenever no curated row is stored, so a missing displayName row can no longer blank a line or empty a forecast. A stored displayName row is still honored as a curated label — it is simply no longer required. See displayName is not gated, but stays a curated label.

One residual gap remains, and it is NOT in this repo: loading a saved ForecastingModel / scenario after its underlying data has degraded restores the stale indication without re-checking readiness. That path lives entirely in the bioloupe-forecasting client — see Recommended hardening and the self-contained handoff docs/forecasting-saved-model-readiness-followup.md.


Single source of truth: ForecastingStatistics::ReadinessChecker::KEY_DATA_REQUIREMENTS (app/services/forecasting_statistics/readiness_checker.rb:25-66). There is no client-side copy of this list: the admin readiness banner renders the server’s computed missing/score from GET /api/forecasting/statistics/readiness (app/javascript/bundles/Statistics/components/MissingDataBanner.jsx), and the forecasting client gates purely by consuming the rows GET /api/forecasting/statistics returns. So this Ruby constant is the one place the gate is defined.

A disease is “ready” only when its guidance rows (is_guidance: true), resolved through the forecasting client’s geo fallback cascade (USA → EU5 → Japan), cover the full checklist for its type. The counts (12 / 7) are simply the checklist lengths and are the same denominators the admin banner shows (e.g. 9/12 essentials).

Common — required for both types (3, all disease-level)

Section titled “Common — required for both types (3, all disease-level)”
VariableLabelMeaning
typeDisease TypeNot data-entered — resolved from the forecasting_indications table (disease_type column) by indication label (ForecastingIndication.type_for_label). An unclassified label fails the gate; a classified one resolves to "Solid Tumor" or "Hematology". See the type-classification rule.
incidenceBaseIncidence BaseNew cases per year (count).
incidenceGrowthIncidence Growth RateAnnual incidence growth, as a percentage (e.g. 1.2 = 1.2%/yr).

type is classified, not gathered (Decision C, 2026-06-16). It is no longer a stored ForecastingStatistic row. The server resolves it from the forecasting_indications table (disease_type column, via ForecastingIndication.type_for_label) and the public /statistics feed synthesizes the type row per ready indication; operators neither enter nor curate it (it was removed from the admin form). So type still counts as a requirement — an unclassified label is not ready — but it is satisfied by the taxonomy classification, mirroring how category is client-derived.

Solid Tumor — 12 total (3 common + 9 type-specific)

Section titled “Solid Tumor — 12 total (3 common + 9 type-specific)”

Solid tumors are modeled with a stage split (early vs. metastatic) plus relapse dynamics, feeding a 5-line funnel (early, met1, met2, met3, met4). (forecasting.ts:748,769-773isHematology === false runs the early+metastatic passes.)

Disease-level (7 = 3 common + 4 stage): type, incidenceBase, incidenceGrowth, earlyStagePercent, metStagePercent, earlyToEarlyRelapse, earlyToMetRelapse

Line-level (5 = 3 transition + 2 DTR):

VariableLineLabelGated on
transitionRatemet21L → 2L Transition Ratepresence
transitionRatemet32L → 3L Transition Ratepresence
transitionRatemet43L → 4L Transition Ratepresence
treatmentRateearlyEarly Stage Treatment Ratepresence
treatmentRatemet1Met 1L Treatment Ratepresence

treatmentRate (DTR) is required only on the first line of each stage (early + met1). The model applies DTR only at stage entry points and skips it on met2/met3/met4 (factor = 1) — see why.

Neither displayName nor category is gated. category is fully client-derived from the line id (deriveLineCategory), so no stored row is needed. displayName falls back to a name derived from the line id (deriveLineDisplayName) when none is stored, so it too is never required — but a stored displayName is still honored as a curated label (see displayName is not gated, but stays a curated label).

Hematology — 7 total (3 common + 4 type-specific)

Section titled “Hematology — 7 total (3 common + 4 type-specific)”

Hematology is modeled without a stage split (no early/met segmentation, no relapse) — a 4-line funnel (line1line4). (forecasting.ts:769-770isHematology === true runs only the therapy pass.)

Disease-level (3): the common set only.

Line-level (4 = 3 transition + 1 DTR):

VariableLineLabelGated on
transitionRateline21L → 2L Transition Ratepresence
transitionRateline32L → 3L Transition Ratepresence
transitionRateline43L → 4L Transition Ratepresence
treatmentRateline11st Line Treatment Ratepresence

treatmentRate (DTR) is required only on the first therapy line (line1) — the model skips DTR on line2/line3/line4. Same rule as solid tumors. Neither category nor displayName is gated for heme (nor for any type): the client derives line{N}"therapy" (deriveLineCategory) structurally, and falls back to a derived line name (deriveLineDisplayName"1st Line" / "2nd Line" / …) when no curated displayName is stored — so neither is required, though a stored displayName is still honored as a curated label.

Per-disease exception — Multiple Myeloma carries a 5th line. MM is the only indication gated beyond the 4-line heme norm: it adds a transitionRate on line5 (4L → 5L Transition Rate, presence), taking its gate total to 8 (7 + 1). Every other hematology disease stays ready on line1line4 and is never gated on a 5th line (it may still render one if a line5 row exists). Single source: DISEASE_SPECIFIC_REQUIREMENTS (readiness_checker.rb); line5’s displayName is not gated either. See CONTEXT.md — “line5 (4L → 5L, Multiple Myeloma only)”.

Why the first line needs no transition rate — and why DTR is first-line-only

Section titled “Why the first line needs no transition rate — and why DTR is first-line-only”

Two different rules, both grounded in the funnel math:

  • transitionRate is inbound — the fraction of the prior line’s pool that flows into this line, stored on the destination line. The first line of each funnel is a stage entry point (fed by the incidence/stage split, not a prior line), so it has no inbound transition. The first line uses baseAddressable with no transition factor; later lines multiply by transitionRate/100 — on the display path in calculateLineFunnelForDisplay (its first-line/later transition branch) and on the time-series path in computeLinePatients’s cascade carry, both in bioloupe-forecasting’s forecasting.ts. → required only on met2/met3/met4 (solid) and line2/line3/line4 (heme). The producer admin enforces this with a destination-keyed transition picker (1L → 2L …, no first-line option) plus a model guard rejecting a transitionRate on line1/early/met1; see CONTEXT.md.
  • treatmentRate (DTR) is applied only at the first line of each stage — DTR has meaning where patients enter the funnel from an addressable pool; on later lines they’re already under treatment, so the model hides DTR in the UI and skips it in computation (factor = 1). Predicate: isDTRApplicable; the gated DTR factor (trFactor) is computed on the display path in calculateLineFunnelForDisplay, and the same predicate gates the DTR step on the time-series path in computeLinePatients — both in bioloupe-forecasting’s forecasting.ts. This was the deliberate 2026-04-16 “hide DTR on non-first lines” change. → required only on early+met1 (solid) and line1 (heme).

Gate alignment (2026-05-29): the readiness gate originally required treatmentRate on every line — over-requiring the later-line DTRs the model discards. It was corrected to require DTR only on first-of-stage lines, matching isDTRApplicable. That took the Tier-1 counts to 12 (solid) / 7 (heme), down from 15 / 10.

Gate alignment (2026-06-05): the gate was hardened to enforce the fields the engine silently needs for a correct forecast: a per-line displayName (presence) and a per-solid-line category (value-checked, resolve-first through the cascade), plus validation that the type value is in RECOGNIZED_TYPES. This took the counts to 22 (solid) / 11 (heme), up from 12 / 7.

Gate alignment (2026-06-16): the per-solid-line category requirement was removed. The forecasting client derives each line’s category structurally from the line id (deriveLineCategory: earlyearly, met{N}metastatic, else→therapy), so a stored category row is redundant and value-gating it gated data the client never reads. This took solid tumor back to 17 (heme unchanged at 11). Zero regression: every indication already carrying forecasting data stays ready, and category is now guaranteed correct by derivation.

Gate alignment (2026-06-17): the per-line displayName requirement was removed. The forecasting client falls back to a name derived from the line id (deriveLineDisplayName: early"Early Stage", met{N}"Metastatic Line {N}", line{N}"{ordinal} Line") whenever no curated row is stored, so an absent displayName can no longer blank a line — the gate was presence-checking a field the client can always supply a non-blank fallback for, so requiring it added no safety. This took solid tumor to 12 and hematology to 7 (from 17 / 11). Zero regression: the derived name is non-blank for every line id, so no line is dropped and no forecast goes empty. Note this differs from category: a stored displayName is not redundant — curated rows carry clinically meaningful labels the generic fallback cannot reproduce (e.g. SCLC Limited Stage/Extensive Stage, GBM Newly-Diagnosed/Recurrent) — so they remain in the data as an optional curated override; they are just no longer gated.


Why displayName and category are not gated

Section titled “Why displayName and category are not gated”

Both displayName and category were briefly gate-enforced (2026-06-05) and then dropped — category on 2026-06-16, displayName on 2026-06-17. They differ in why: category is structurally derived (uniform per line id), so a stored row is genuinely redundant; displayName is derived only as a fallback, so a stored row is never required but stays a meaningful curated label. They are called out here because neither reason is obvious from the requirement list.

displayName is not gated, but stays a curated label

Section titled “displayName is not gated, but stays a curated label”
  • When no curated row is stored, the client derives a line’s display name from its id: deriveLineDisplayName (bioloupe-forecasting line-metadata.ts:48-58) returns early"Early Stage", met{N}"Metastatic Line {N}", line{N}"{ordinal} Line", and the raw id for anything else — non-blank for every funnel line id.
  • The disease-config path uses a stored displayName row only as an override-with-fallback: deriveDiseaseConfig takes the curated row when present and falls back to deriveLineDisplayName(lineId) when none is stored (reference-query.ts:443-444). A line therefore always carries a non-blank displayName.
  • The instance/snapshot path seeds each line’s lineName row from that config value (init-flows.ts:466-467), and the line materializer drops only lines whose name is blank (selectors/lines.ts:150-151if (!lineName) continue;). Because the derived name is never blank, no line is dropped.
  • Why it’s no longer gated (2026-06-17): the engine can no longer end up with a nameless line — deriveLineDisplayName guarantees a non-blank fallback — so the silent empty-forecast failure the gate once guarded against cannot occur, with or without a stored row. Removing the gate is zero-regression and lets indications without curated displayName rows enter the feed. A stored displayName still wins over the derived fallback (reference-query.ts:443-444), and curated rows carry clinically meaningful reference labels the generic fallback cannot reproduce (e.g. SCLC Limited Stage/Extensive Stage, GBM Newly-Diagnosed/Recurrent with a line-id offset, prostate mCRPC, Merkel IO regimen, melanoma Adjuvant) — so they stay in the data as an optional curated override, not deleted. Operators are never required to enter a displayName. (Runtime end-user line renaming applies only to user-added custom lines, LinePanel.tsx:582-599; reference lines are read-only, so these curated rows are the sole source of those clinical labels.)
  • The client derives each line’s category structurally from its id: deriveLineCategory (bioloupe-forecasting line-metadata.ts:34-38) returns early"early", met{N}"metastatic", anything else (line{N})→"therapy". category is uniform per line id across all diseases, so the derivation is behavior-preserving for real producer data.
  • The disease-config path derives it unconditionallyderiveDiseaseConfig calls deriveLineCategory(lineId) and no longer reads a stored row (reference-query.ts:438-440,447, comment: “drops the dependency on a stored category row”). The instance/snapshot path uses a stored category row only as an override-with-fallback (lines.ts:169-174: (stored category) ?? deriveLineCategory(id)), so a snapshot lacking the row still resolves correctly (bug #10).
  • The funnel builder still filters lines by exact category: forecasting.ts:755lines.filter((l) => l.category === category), running only the early and metastatic passes for a solid tumor (forecasting.ts:771-773), with addressable routing early→earlyAddressable, metastatic→metAddressable, therapy→totalAddressable (forecasting.ts:688-704). The difference is that the right value is now guaranteed by derivation rather than by a gated stored value.
  • Why it’s no longer gated (2026-06-16): because the client always derives a correct category, a stored row is redundant — the old gate value-checked data the client never reads. Removing it is zero-regression (the derivation matches what correct data carried) and lets indications that lack curated category rows enter the feed correctly. Hematology was never gated on category (its single therapy pass legitimately draws from totalAddressable); this change makes solid tumors consistent with that.

Solid TumorHematology
Common disease-leveltype, incidenceBase, incidenceGrowthtype, incidenceBase, incidenceGrowth
Stage variablesearlyStagePercent, metStagePercent, earlyToEarlyRelapse, earlyToMetRelapsenone (no stage split)
Therapy lines5 (early, met1met4)4 (line1line4)
transitionRate (gate)met2, met3, met4line2, line3, line4
treatmentRate (gate)early, met1 (first-of-stage only)line1 (first line only)
displayName per linenot gated — curated label, derived fallback (deriveLineDisplayName)not gated — curated label, derived fallback (deriveLineDisplayName)
category per lineclient-derived, not gated (early→early, met{N}→metastatic)client-derived, not gated (line{N}therapy)
Gate total127
Per-disease extranoneMultiple Myeloma only: +transitionRate on line5 (4L → 5L) → MM gate total 8

How the system itself classifies a model: a scenario with stage rows (earlyStagePercent) is solidTumor, otherwise hematology (app/services/forecasting_model_summary.rb:29,36).


What happens when a variable is absent (engine defaults)

Section titled “What happens when a variable is absent (engine defaults)”

Verified in bioloupe-forecasting deriveDiseaseConfig (in reference-query.ts) and the math engine. Every gated field below is gate-enforced, so its absence (or, for type, a wrong value) hides the disease instead of letting the engine run on the default — the right column is what would happen if the gate were bypassed. The exceptions are displayName and category, which are no longer gated: category is structurally derived, and displayName falls back to a derived name when no curated row is stored (see their rows).

FieldIf missing, the model…Severity (now gate-blocked)
typereturns null — no config built (the only return null in deriveDiseaseConfig — the if (!type) return null guard); an unrecognized value also fails the gateHidden by gate (would hard-stop — Configuration card spins forever, Configuration.tsx:154-165)
incidenceBasedefaults to 0 (the incidenceBase resolver in deriveDiseaseConfig) → entire funnel is zeroHidden by gate (would be a silent zero forecast)
incidenceGrowthdefaults to 0.5%/yr (DEFAULT_INCIDENCE_GROWTH_RATE, constants.ts:110; applied in deriveDiseaseConfig)Hidden by gate (would invent an assumption)
earlyStagePercent (solid)the stage-mix block is skipped (the incidenceConfig.stageMix block in deriveDiseaseConfig) → no early/met segmentationHidden by gate (would break the solid funnel)
metStagePercent (solid)defaults to 0 (the metStagePercent ?? 0 default in deriveDiseaseConfig)Hidden by gate (would treat all incidence as early)
earlyToEarlyRelapse / earlyToMetRelapsedefault to 0 (the ?? 0 defaults in deriveDiseaseConfig’s stageMix block)Hidden by gate (would zero out relapse flow)
transitionRate (per line)defaults to 100% (the transitionRate ?? 100 default in deriveDiseaseConfig)Hidden by gate (would advance everyone)
treatmentRate (first-of-stage line)defaults to 100% (the treatmentRate ?? 100 default in reference-query.ts); on later lines the value is ignored (factor = 1, isDTRApplicable)Hidden by gate (would treat everyone at stage entry)
displayName (per line)stored curated row wins; falls back to a name derived from the line id (deriveLineDisplayName) when absent → non-blank either way (applied in deriveDiseaseConfig)Not gated — the derived fallback is non-blank, and a curated row remains an optional override (line-metadata.ts:48-58)
category (per line)client-derived from the line id (deriveLineCategory) → correct value with or without a stored rowNot gated — derivation is behavior-preserving (line-metadata.ts:34-38)

Only type blocks engine execution outright; everything else “runs” but fabricates the answer from defaults. The gate covers the gated fields above — including the value of type; displayName and category need no gate because the client always resolves both (a derived category, and at least a non-blank derived fallback for displayName). The only silent-garbage path left is a saved-model reload (see below).

Why incidenceGrowth stays gate-required even though it defaults: unlike later-line treatmentRate (whose value the model discards), incidenceGrowth is consumed — the 0.5%/yr fallback silently substitutes a made-up growth rate across a multi-year projection. Keeping it required forces a real, sourced value. It is the one field that is both gate-required and safely-defaulted; that is intentional, not a waste-mismatch like the later-line DTRs that were removed.


  1. Geo = USA is the cascade root, not a hard floor. Readiness starts at geo = 'USA' (ReadinessChecker::GEO_FOR_READINESS, readiness_checker.rb:14) but resolves each field through the client’s geo fallback cascade (USA → EU5 → Japan, GEO_FALLBACK_CASCADE :18; applied in cascade_scope :169-171). So a field present only under EU5 or Japan does count toward readiness — it still feeds the USA forecast, exactly as the client resolves it. (Real example: OPC, whose met3/met4 transition rates are EU5-sourced, is ready.) EU5/Japan data is therefore not merely optional.
  2. Canonical rows only (is_guidance: true). A disease with only alternative rows is not ready (readiness_checker.rb:171; test T5).
  3. Existence, not recency. Only presence is checked; year is ignored (metadata). A value from any year satisfies the requirement. (category is no longer a value-checked exception — it was dropped from the gate on 2026-06-16; see category is client-derived, not gated.)
  4. type is classified from the label; an unclassified label fails the gate. type is not a stored row — it is the solid/heme classification read from the forecasting_indications table (disease_type column) keyed by the indication label (ForecastingIndication.type_for_label), checked against RECOGNIZED_TYPES (['Solid Tumor', 'Hematology'], readiness_checker.rb:23) in missing_for (readiness_checker.rb:143,147). A label outside the classification resolves the type-specific checklist to empty and fails the common type requirement, so the disease is hidden — it can no longer slip through “ready on the 3 common variables alone.” A stray stored type row is ignored, and the controller’s type_unknown flag and ready agree by construction (statistics_controller.rb:59-60).
  5. The API enforces the gate (with one carve-out). GET /api/forecasting/statistics returns rows only for ReadinessChecker-ready diseases (statistics_controller.rb:11) — plus population-marker rows (popBase/popGrowth) at indication = '', the only names allowed to pass at a blank indication (statistics_controller.rb:15-17); any other name at indication = '' is excluded (enforced by the model validation population_markers_only_at_blank_indication and the public-feed scope). The admin endpoint GET /api/forecasting/admin/statistics returns everything regardless of readiness.
  6. Computed live. There is no cached forecasting_ready column — readiness is recomputed per request (db/structure.sql is the authoritative schema; no such column exists).

Is the gate actually wired? (“does an incomplete indication stay hidden?”)

Section titled “Is the gate actually wired? (“does an incomplete indication stay hidden?”)”

Mostly — the dropdown is genuinely gated; one residual path (saved-model reload) can still bypass it.

  • Implemented (dropdown path): client fetches the gated endpoint (useStatistics.ts:47), derives the indication list purely from those rows (indicationsAtom, selectors/geo-indication.ts:27-30), and the dropdown renders only that list (Forecasting.tsx:146-149,380-387). No hardcoded list, no URL/deep-link override, and the client computes no independent readiness — gating is purely a consequence of the server returning only ready rows. So an unready disease cannot appear in a fresh dropdown selection. ✅
  • Residual leak (saved models / scenarios): loading a saved ForecastingModel (or duplicating a scenario) restores the saved activeIndication and instanceRows verbatim (model-atoms.ts:119-121), validating only structural shape (FullSnapshotSchema) — never current readiness. If a disease that was ready at save time later loses required data, reloading resurrects the unready indication: the Model/Sales/Monte-Carlo sections render from stale snapshot rows while the Configuration card spins forever (live type is gone). This is the one remaining hole and lives in the bioloupe-forecasting client — full diagnosis + fix direction in docs/forecasting-saved-model-readiness-followup.md. ⚠️

What is genuinely optional (safely defaulted or feature-gated)

Section titled “What is genuinely optional (safely defaulted or feature-gated)”
Not requiredWhy
Biomarkers (biomarkerName / biomarkerPercentage / biomarkerTestingRate, bm:<slug> rows)Disease-specific add-ons; not every cancer has them.
Population (popBase, popGrowth)Used only for per-capita normalization when a geo fallback fires; degrades to a raw copy if absent (reference-query.ts:331-362). Not read by the core forecast.
healthcareAccessDefaults to 95% (reference-query.ts:365).
unknownStagePercentAuto-derived as max(0, 100 − early − met) when missing/0 (reference-query.ts:395-398).
Stage sub-splits (earlyStageLocalizedPercent, earlyStageLocallyAdvancedPercent)Optional refinement; if both present, summed into earlyStagePercent (reference-query.ts:384-387).
transplantEligible / transplant flowOpt-in feature behind per-line UI toggles + a hardcoded indication allowlist; absence is a no-op (transplant.ts:54-59).
monthsOfTherapy / compliance (per-line)Sales-layer guidance — validated when present (monthsOfTherapy 0..600, compliance 0..100; forecasting_statistic.rb) but not gate-required. They affect the revenue layer only, not patient flow; the client seeds each per-line from a curated guidance reference row, falling back to 12 / 85 when absent (reference-query.ts deriveDiseaseConfig).
Pricing (forecasting_pricings)The patient-flow forecast runs without pricing (forecasting.ts:1151); only the revenue layer needs it. Out of scope for “the model runs.”
displayName (all lines, both types)The client derives a non-blank fallback name from the line id (deriveLineDisplayName); a stored row is an optional curated override, never required and not gated (dropped 2026-06-17).
category (all lines, both types)The client derives it structurally from the line id (deriveLineCategory); a stored row is redundant, so it is neither required nor gated (dropped 2026-06-16).

Both displayName and category are listed above because neither is a value an operator must supply — category is always derived, and displayName falls back to a derived name when absent. A stored displayName row acts as an optional curated override (and carries clinical labels the fallback cannot reproduce).


Section titled “Recommended hardening (to make “ready” actually mean “forecastable”)”

The type-value validation is implemented in the gate (2026-06-05). The category and displayName requirements were added then too but removed on 2026-06-16 and 2026-06-17 respectively: category is structurally derived (its stored row is redundant), and displayName always has a non-blank derived fallback (so gating its presence added no safety, though a stored curated row is still honored). One gap remains, and it lives in the bioloupe-forecasting client, not here:

  1. Re-validate saved models on load: after restoring a scenario, check activeIndication against the current ready set (or call GET /api/forecasting/statistics/readiness) and clear/flag it if no longer ready, instead of silently restoring stale rows. Self-contained diagnosis, anchors, and fix direction: docs/forecasting-saved-model-readiness-followup.md.

ConcernLocation
Gate-enforced must-have listapp/services/forecasting_statistics/readiness_checker.rb:25-66 (KEY_DATA_REQUIREMENTS)
Admin banner (server-driven, no client mirror)app/javascript/bundles/Statistics/components/MissingDataBanner.jsx → fetches GET /api/forecasting/statistics/readiness
Recognized typesReadinessChecker::RECOGNIZED_TYPES = ['Solid Tumor', 'Hematology'] (readiness_checker.rb:23)
type classification (solid/heme by label)readiness_checker.rb:143,147 (in missing_for) + the forecasting_indications table (disease_type column), via ForecastingIndication.type_for_label / .type_map (app/models/forecasting_indication.rb)
category derivation (client)bioloupe-forecastingsrc/features/reports/forecasting/atoms/primitives/line-metadata.ts:34-38 (deriveLineCategory); applied in deriveDiseaseConfig + linesAtom (selectors/lines.ts)
displayName derivation (client)bioloupe-forecastingsrc/features/reports/forecasting/atoms/primitives/line-metadata.ts:48-58 (deriveLineDisplayName); applied in deriveDiseaseConfig, seeded into the snapshot at actions/init-flows.ts:466-467
Readiness teststest/services/forecasting_statistics/readiness_checker_test.rb
Server gate / carve-outapp/controllers/api/forecasting/statistics_controller.rb:10,20
Config builder + engine defaultsbioloupe-forecastingderiveDiseaseConfig in src/features/reports/forecasting/atoms/primitives/reference-query.ts
Engine: line drop on blank name (never fires for derived names)bioloupe-forecastingsrc/features/reports/forecasting/atoms/selectors/lines.ts:150-151
Engine: category → addressable routingbioloupe-forecastingsrc/features/forecasting/math/forecasting.ts:688-704,748-774
DTR applied first-of-stage only (why later-line treatmentRate isn’t gated)bioloupe-forecastingisDTRApplicable in forecasting.ts (gated trFactor factor in calculateLineFunnelForDisplay)
Dropdown gating chainbioloupe-forecastinguseStatistics.ts:47selectors/geo-indication.ts:27-30Forecasting.tsx:146-149
Saved-model bypass (hole c, follow-up)bioloupe-forecastingmodel-atoms.ts:119-121; see docs/forecasting-saved-model-readiness-followup.md
All variable names + labelsForecastingStatistic::VARIABLE_NAMES (app/models/forecasting_statistic.rb:31)
Canonical disease/indication labels + acronymsthe forecasting_indications taxonomy table (indication column), seeded from SeedForecastingIndicationsThor::PAIRS (lib/tasks/one_off/seed_forecasting_indications.thor); full label → acronym list rendered in docs/forecasting-disease-acronyms.md
Design rationale (transition/line conventions)CONTEXT.md (project glossary — transition-rate convention + Multiple Myeloma-only line5)

Maintenance note: KEY_DATA_REQUIREMENTS (readiness_checker.rb) is the single source of truth. The admin banner renders the server’s computed missing/score from GET /api/forecasting/statistics/readiness, and the forecasting client gates only by consuming the rows GET /api/forecasting/statistics returns — neither keeps an independent requirement list, so a gate change needs no client edit.