Skip to content

Technical documentationInternal

For the team and anyone taking the product over. Not linked from the site and not indexed.

Technical documentation pages

Pipeline

Pipeline (registry slug drug-pipeline-explorer) downloads a condition's registered interventional drug, biologic and genetic trials from ClinicalTrials.gov, counts and classifies them in the browser, lists the US products whose FDA label names the condition from openFDA, and, in its FDA precedent view, joins those products to FDA's own approval records. This page describes the code as it stands: files, data flows, every rule and constant, the shipped data, the tests and the known gaps.

Purpose and users

The app answers "what is the registered clinical-development landscape for this condition, and what has the FDA approved for it?". The registry describes it as "Trials for any condition from ClinicalTrials.gov, US approvals from openFDA, and the FDA precedent behind them" (content/apps/drug-pipeline-explorer.ts, entry.tagline).

Intended users, from the design documents (intent, not measured): competitive-intelligence and business-development analysts, strategy and investment staff, and, for the FDA precedent view, regulatory-affairs staff at small and mid-sized biotechs and consultancies (docs/design/prd-drug-pipeline-explorer.md; docs/design/brief-precedent.md §2).

What it is not, as the registry's limits state (content/tools/drug-pipeline-explorer.ts, detail.limits): not medical, investment, regulatory or legal advice; not a complete commercial asset inventory (trial counts are not drug or programme counts); not a forecast. The registry sets notMedicalAdvice: true and statements: { investmentAdvice: true, forecasts: true, regulatoryAdvice: true }. The scientific audit (docs/science/2026-10-scientific-audit.md, "Pipeline") asks that the trial-versus-asset distinction stay prominent and that the modality rules be presented as fallible text heuristics.

Status, version and governing decisions

Item Value Where
Product name "Drybench Pipeline", short "Pipeline" content/app-names.ts
Status and version status: "preview", version: "0.2.0", updated: "2026-10-24" (Stage G; changelog entry of 24 October 2026) content/apps/drug-pipeline-explorer.ts, content/apps/changelog/drug-pipeline-explorer.ts
Accent and mark violet, funnel content/apps/identity.ts
Workspace group discovery content/apps/identity.ts
Workers, WebGL, share-alike data all false entry.flags

Decisions that shape the app:

  • D-0009 (superseded) kept the BIO/Informa/QLS historical success-rate tables through the carry-over. D-0011 replaced them with the FDA precedent view (phase A of docs/design/brief-precedent.md), removed every trace of them and added scripts/removed-sources.mjs, which fails CI on any return of the patterns (bio-2021, "QLS Advisors", "Informa Pharma Intelligence" and others). Old links with view=odds open FDA precedent (see state in the address).
  • D-0022 to D-0024 define the J3 guided walk ("From a compound to an IC50"). That walk runs through Calc, Assay and the compound's page; it does not pass through Pipeline, and Pipeline has no guided walk of its own. The PRD cut a walkthrough overlay (docs/design/prd-drug-pipeline-explorer.md A14; build spec §1.3). The on-ramp is the welcome start with seven example searches.
  • D-0028, D-0036 to D-0040 set the shared shell, sidebar, theme and object-first layout that the workspace follows (see Shared shell).
  • D-0045: viewing records nothing; only an object the person already holds is refreshed (src/apps/drug-pipeline-explorer/learn.ts header).
  • D-0116 and D-0117 (Stage G, 23 October 2026) started and continued the depth stage: export scope, cited FDA precedent, acquisition dates and saved views (docs/progress/pipeline-stage.md; the review docs/reviews/2026-10-24-codex-pipeline-review/README.md). SCI-247 (24 October 2026) labels every product date by its source (Display dates by source).
  • Wave 4 (the discovery line, D-0041 and D-0053; merged 11 October 2026) added registry coverage, the timeline by phase and two exported figures (step 4 in docs/progress/wave4-discovery.md; CPO Wave 4 §2.6; the CSO's and CTO's Wave 4 memos). The registry entry's version and date were not changed.

The build specification (docs/design/build-spec-drug-pipeline-explorer.md) is a historical R1 specification. Its own 7 October 2026 preface says the earlier proposals are not marked as shipped. Many of its items were not built; they are listed under known gaps.

Addresses and entry points

Address What it serves
/pipeline The product page (marketing route, registry copy in content/apps/drug-pipeline-explorer.ts page)
/pipeline/app The workspace (src/apps/drug-pipeline-explorer/entry.tsx, loaded by src/apps/definitions.ts only on this app's route)
/pipeline/app#s=... The workspace with state in the fragment (see below)
/pipeline/app?condition=... Frozen legacy query, accepted and rewritten to the fragment before any request
/tools/drug-pipeline-explorer Redirects to the workspace (legacyPath in content/apps/identity.ts; see Deployment and environment)
/docs/... for the app Public docs from content/apps/drug-pipeline-explorer.docs.ts, with a generated verification section (src/components/suite/marketing/verification.marketing.tsx, reading content/apps/drug-pipeline-explorer.verification.ts and the four precedent fixtures)
Object pages for condition, trial, product Pipeline's object tabs (src/apps/drug-pipeline-explorer/object-tabs.ts, object-tab.tsx)

The URL segment pipeline comes from APP_SEGMENTS in content/apps/routes.ts. The app has no extra static pages (page-ids.ts exports an empty list).

Other entry points into the workspace:

  • Protein's drug drawer links a disease to Pipeline (src/components/tools/protein-explorer/drugs-panel.tsx, host.links["drug-pipeline-explorer"]({ condition })), arriving as /pipeline/app?condition=...&obj=cond%3A...&via=protein-explorer (asserted in e2e/objects/send-to.spec.ts, "L2").
  • Protein's Evidence tab links each associated disease the same way (src/apps/protein-explorer/views/dossier/evidence-cards.tsx, conditionQuery of the Open Targets name; the drawer's rows too), received by the search rule above (asserted in e2e/drug-pipeline-explorer/protein-handoff.spec.ts: the arrival strip, no mine in any query string, the state kept on reload, the Did-you-mean start for words with no exact match and no trials, also while the catalogue loads and when it fails).
  • A condition object page opens the workspace through objectInbound in src/apps/drug-pipeline-explorer/objects.ts (params: { condition: o.label || o.id }).
  • The command palette's seven "example" commands and the rail's Examples section call searchPreset.
  • A saved research view reopens through the mine key (see Objects, Studies and hand-off).
  • The product page renders a live preview: PipelineWorkspace({ preview: true }) keeps its state in memory (useMemoryUrlState in views/workspace.tsx), opens the default example, learns no objects and shows no object actions.

File map

App layer: src/apps/drug-pipeline-explorer/

File Role
entry.tsx Client entry; hands the definition and the full registry entry to the shell. displayName drybench-entry:drug-pipeline-explorer is the marker scripts/size-check.mjs finds the chunk by
definition.ts AppDefinition: Main, rail, inspector "selection" (hidden until explicitly opened, dockMinMain 60rem; D-0060), view shortcuts, palette commands, sources for the status bar, / search focus, object tabs, share link
workspace.tsx PipelineMain: the workspace via next/dynamic (ssr: false) with a skeleton and the suite's ToolBoundary
views/workspace.tsx The workspace: opening state, inspector control, actions, URL writing, Back and Forward, saved research views, rail, live-region sentences, view switching, the coverage line. Its displayName drybench-island:pipeline-workspace is the workspace chunk's size marker
views/landscape.tsx The Landscape view frame and the Board/Console layout switch, with a skeleton per mode
views/board.tsx Board ("Cards"): five lanes, Phase 1, Phase 2, Phase 3, US approved, Phase 4; BoardFacts (headline figures)
views/console.tsx Console ("Matrix"): figure strip, phase by status matrix, starts timeline with the "All trials / By phase" control (tl) and the phase key
views/matrix.tsx The phase by status matrix and its phone layout
views/timeline.tsx Start-year columns with US approval pins and clustering; stacked by phase with patterns when tl=phase; the data table gains a column per phase
phase-marks.ts Pure: each phase bucket's chart slot and stroke pattern (phasePatternPath, TILE 6 px), phaseColumns; shared by the timeline and the figure writer
views/trials.tsx Trials view: the suite DataTable of trials, columns, default sort
views/charts.tsx Charts view: modality over time, durations, sponsors, stop reasons; cross-filter presses
views/approvals.tsx US approvals view
views/precedent.tsx FDA precedent view, its filters, KPI tiles, table, cards and the inspector panel PrecedentPanel; own chunk PrecedentView.displayName drybench-island:pipeline-precedent marks its next/dynamic chunk for pipeline-precedent-chunk, 18.9 KB (REL-549)
views/precedent-data.ts FDA precedent's loaders: the four snapshots, the live letter count, newer letter files, label studies
inspector.tsx Inspector panels: condition, trial, product, precedent product (dynamic), approvals cluster, matrix selection, filters
records.tsx TrialRecord, ProductRecord, MoleculeLinks, shared by the inspector and the object tabs
object-tab.tsx, object-tabs.ts Object page tabs for condition, trial and product, each loaded on first open
objects.ts Pure: object inputs and refs (cond:, nct:, fda:), inbound mapping, Molecule payload
learn.ts Hooks that hand objects to the shared store (useLearn)
landscape-model.ts Pure: headline figures, stages, card orders, combined-phase bridges, matrix and picks, timeline pins and clustering, name formatting (tidyName)
xfilter.ts Pure: the cross-filter (xf) grammar, subsets and wording
phases.ts Pure: phase slugs and the four trial stages
url-schema.ts Pure: the fragment schema, validators, aliases and helpers
store.ts The workspace's published snapshot for shell-rendered parts (useSyncExternalStore)
exports.ts, exports-run.ts Export menu items; the CSV writer, loaded on demand (runPipelineExport.displayName drybench-export:pipeline-writer marks its chunk for pipeline-writer-chunk, 14.9 KB; REL-549)
exports-provenance.ts The provenance block, the ClinicalTrials.gov and openFDA provenance sources and trialsData, shared by the CSV writer and the figures
exports-figure.ts The two figures' SVG, size, provenance, file base and caption (FigureKind timeline or sponsors); loaded in the items' prepare (chunk marker drybench-export:pipeline-figures)
provenance.ts Pure: ClinicalTrials.gov and openFDA provenance for chips
precedent-provenance.ts Pure: claim-level citations for every precedent cell; productDateCell cites a product's own date to its source (Drugs@FDA, the NDC Directory, or openFDA for a saved date of unrecorded type). It composes values only: every short label and citation sentence is in content/apps/drug-pipeline-explorer.precedent-citations.ts (only this module imports it, so it loads with the precedent and writer chunks, not the definition), workspaceCopy.precedent.mark and workspaceCopy.dates (REL-559), and precedent-provenance.test.ts checks each string as written
display-dates.ts Pure: how a product's date reads by its source (original approval, NDC marketing start, type not recorded, not loaded, none): the Board card, the timeline's pins and note, the US approvals and record sublabels, and the FDA precedent cell and inspector (SCI-247; the CSO's ruling of 24 October 2026)
not-found.ts Pure: when a search is "not found" rather than a subject (searchOutcome: subject, not-found or pending on the catalogue's state); the inbound rule for a condition= another app sends
inspect-words.ts Pure: inspector titles and kickers
workspace-inputs.ts Pure: the saved research-view payload and its validator
layout.ts DOCK_MIN_MAIN = "60rem"
palettes.ts Re-exports the status and modality palettes for src/components/suite/tokens.test.ts
page-ids.ts Empty list of app pages
parts/* Header, search form, welcome, status block, status marks, view header and nav, chart card, count-up, empty result, motion classes; coverage-line.tsx (the coverage line) and coverage-details.tsx (its details, on demand, marker drybench-island:pipeline-coverage)
*.test.ts Unit tests (see Verification)

Domain layer: src/components/tools/drug-pipeline-explorer/

These modules are pure (no React, no browser globals, relative imports) unless marked, so Node tests and the pipeline scripts import them directly.

File Role
ctgov.ts ClinicalTrials.gov API v2 query building; Trial, phase bucketing, statuses, partial dates (re-exports PHASE_BUCKETS)
ctgov-parse.ts parseStudy, parsePage: API studies into Trial records (moved out of ctgov.ts on 11 October 2026 so the query builder travels without the modality classifier)
phase-buckets.ts PhaseBucket and PHASE_BUCKETS, with no imports, so a key or figure names the phases without the parser
fetchers.ts Network calls shared by the browser and pipeline/ scripts: fetchJson, fetchTrials, fetchStudy, fetchProduct, fetchVersion, fetchApprovals, mapSettled
openfda.ts openFDA label search construction, URLs, parsers, product grouping, negation reading, NDC tracing
modality.ts The modality classifier
aggregate.ts The aggregations behind every chart, tile and snapshot
why-stopped.ts Stop-reason grouping
sponsor-aliases.ts, sponsor-aliases-version.ts Lead-sponsor merging rules; SPONSOR_ALIASES_VERSION (sponsor-aliases-2026-10-05), named in the sponsor figure's caption, in an import-free module
filters.ts Draft filter validation, description and legacy URL parameters
presets.ts The seven example searches and their hand-checked label queries
snapshot.ts Example-snapshot types, URLs, 60-day rule, knownFromSnapshot
use-pipeline.ts Client hook: query state, the trials and approvals flows, caches, snapshot fallback
format.ts, theme.ts Formatting; chart colour roles (STATUS_ROLE, MODALITY_ROLE)
chart-model.tsx, charts.tsx Chart data model and the hand-written SVG chart kit (client)
approvals-panel.tsx Approvals headline, caveats, detail and modality wording, shared by the US approvals, Board, precedent and records views (the unused ApprovalsPanel component was removed, REL-552)
precedent/types.ts Snapshot envelope and row types; PRECEDENT_FILES
precedent/nme.ts Compilation join, PrecedentRow, figures, filters
precedent/review-time.ts Review time and missing reasons; REVIEW_TIME_METHOD
precedent/designations.ts Flag and review-designation parsing; breakthrough lists
precedent/crl.ts Complete response letter join and the live count
precedent/letters.ts stripLetter: the CRL field allow-list
precedent/pivotal.ts NCT numbers from a label's Clinical Studies section
precedent/confirm.ts Project Confirm name join; built and tested, imported by nothing in the app
precedent/snapshot-age.ts 90-day precedent staleness rule
precedent/fixtures/*.json Draft verification fixtures (see Verification)

Content, pipeline and data

Path Role
content/apps/drug-pipeline-explorer.ts Registry entry (sources, sourceMeta, excluded sources, endpoints, budgets, objects, cross-links) and product-page copy; PRECEDENT_REQUESTS
content/apps/drug-pipeline-explorer.entry.ts Full entry with identity, imported by the app chunk
content/tools/drug-pipeline-explorer.ts detail: method text, the eight sources, the 16 limits
content/apps/drug-pipeline-explorer.workspace.ts All workspace copy (693 lines)
content/apps/drug-pipeline-explorer.objects.ts Object tab copy
content/apps/drug-pipeline-explorer.discovery.ts Wave 4 copy, read by the workspace and figure chunks only: coverageCopy, timelineCopy, figureCopy
content/apps/drug-pipeline-explorer.verification.ts The four verification sets for the docs page
content/apps/drug-pipeline-explorer.docs.ts Public docs
pipeline/drug-pipeline-explorer/snapshot.ts Writes the seven example snapshots and index.json
pipeline/drug-pipeline-explorer/verify_stems.py Checks every classifier stem against the WHO stem book
pipeline/drug-pipeline-explorer/freeze-ctgov.mts, coverage_oracle.py, fixtures/ctgov/ Wave 4: a frozen ClinicalTrials.gov search (cystic fibrosis, 11 October 2026) and the independent Python oracle for coverage, the timeline by phase and the sponsor figure (see Verification)
pipeline/drug-pipeline-explorer/precedent/ Five snapshot scripts, the weekly verify.ts, fixture builders
public/data/drug-pipeline-explorer/snapshots/ Seven example snapshots plus index.json
public/data/drug-pipeline-explorer/precedent/ Five FDA precedent snapshots
e2e/drug-pipeline-explorer/ Four browser specs and their fixtures

Workspace structure

Shell integration

definition.ts registers:

  • Views and keys: l Landscape, t Trials, c Charts, a US approvals, p FDA precedent (all but l only once a condition is searched), v switches Board and Console on the Landscape, , and . step the open trial through its list.
  • Commands: one per example ("pipeline.example.<id>"), switch to Board or Console, open the filters, clear the filters, clear the chart filter.
  • Status-bar sources: ctgov with dataAsOf from ClinicalTrials.gov's dataTimestamp and state: "live" or "snapshot".
  • Share: available once a condition is searched; the link is the workspace's own href() (the address bar is written at most every couple of seconds), with mine removed.

The workspace publishes its state after every render through store.ts; the rail, inspector, palette, keys and export menu read it with readWorkspace() or useWorkspace(). WorkspaceActions (setView, setMode, toggleMode, inspect, openTrial, stepTrial, pick, openTrials, focusSearch, clearChartFilter, searchPreset) is the only way shell-rendered parts drive it.

Page layout

With nothing searched, or when a search is not found, the workspace shows parts/welcome.tsx: the shared compact discovery start, the search field, and the seven examples as chips. The shell renders the single limits notice below this content (D-0060; the CEO's feedback of 14 October 2026); Welcome no longer accepts an unused notice prop. Otherwise the order is: ConditionHeader (one subject panel with search as its first strip, H1, BoardFacts, provenance; UI-15; since the scientist-workflow merge 836907fc of 24 October 2026 the Save and object controls sit on the section tab row, ViewNav's actions). BoardFacts' "US products" counts the products for the condition only (productCounts(…).indicated); its description (title and screen-reader term, header.productsTerm) names the labels left out, ruled out and patient group apart, and any population candidates left unread. The inspector's condition panel shows the same figure with the same description (the CSO's memo docs/board/memos/2026-10-17-cso-header-product-count.md). The approvals summary and the Board's approved lane still count the full list of labels that mention the condition and break it down, the saved source date if a saved view was reopened, ViewNav, StatusBlock and the view region.

While a search for another condition runs, the previous result stays on screen, dimmed and inert, and the header says what is still shown (elsewhere in views/workspace.tsx). A failure clears it.

conditionNotFound (not-found.ts) treats a search as not found only when all of these hold: the condition catalogue has loaded, the search is not an example, filters are default, the live result is finished with no partial error, totalCount and the trial list are both zero, and findCondition does not recognise the text. It then offers the best local match from suggestConditions as a "Did you mean" key in the danger-toned message under the field. Focus returns to the field; while the message or a condition error stands, focus does not open the suggestion list (openOnFocus on SuggestionInput), so the list never covers them; typing or an arrow key opens it. A known condition, an example or a filtered search with no trials is still a subject with an empty landscape. Since the CPO's E5 ruling of 17 October 2026 the rule is searchOutcome(search, catalog), which conditionNotFound wraps: it returns subject, not-found or pending, with the catalogue's state (loading, ready or unavailable, from useCatalogs) passed in. A finished, unfiltered, non-example search with no trials is pending while the catalogue loads, so the workspace shows the start, never the empty landscape with Save on it, and announces nothing; when the catalogue could not load it is not-found and unchecked, with no suggestion, the message welcome.notFoundUnchecked ("No trials match …. The list of condition names did not load, so similar names cannot be suggested. Check the spelling, or try again to load the names.", the CPO's wording of 18 October 2026) and a "Try again" key (catalogTexts.retry, wired to useCatalogs().retry in parts/search-form.tsx). The workspace's outcome depends on the hook's status, so a retry that loads the names re-decides the same finished search without a new request: Did you mean, or the landscape for an exact match. The start's alert stays the one live region. The same rule is the inbound rule for a condition another app sends: Protein's indication links and Evidence disease rows (condition=) are searched as typed, an exact catalogue match or any trials open the landscape, and words with no exact match that find no trials keep the Did-you-mean start (not-found.test.ts; e2e/drug-pipeline-explorer/protein-handoff.spec.ts).

Landscape: Board and Console

The mode comes from the link's lv, else this device's stored choice (storageKey(SLUG, "landscapeView")), else Board. Once a result is on screen the workspace always writes lv into the link, so a copied link names the layout it was copied from. The visible labels are "Cards" (board) and "Matrix" (console) (copy.landscape.modes). They are a two-button switch (segment buttons with aria-pressed, in a group named "Landscape view") beside the Landscape heading, not a menu (the CEO, 14 October 2026); after v moves focus out of a replaced body it lands on the pressed button.

Board (views/board.tsx, model in landscape-model.ts):

  • Five lanes: Phase 1 (Early phase 1, Phase 1), Phase 2 (Phase 1/2, Phase 2), Phase 3 (Phase 2/3, Phase 3), US approved, Phase 4 (STAGE_PHASES in phases.ts). A combined phase sits with the later stage. Trials with phase "Not applicable" are left off the Board.
  • Each lane shows its total and status strip from the aggregates (every analysed trial), then three cards (SHOWN = 3), eight when expanded (MORE = 8). Cards come from the trials in hand: every analysed trial for a live search, only the 100 recentTrials for a snapshot.
  • Card orders (CARD_ORDERS): newest (trials started, or planned within 366 days, first; starts further ahead next; withdrawn trials last), largest (enrolment descending), industry (industry-led only, newest order). The choice is stored per device (cardOrder key).
  • Between Phase 1 and 2, and between 2 and 3, a connector shows the count of trials registered as Phase 1/2 or Phase 2/3 (bridgeInto). It is a count of registered trials, never a rate, because the registry does not link a trial in one phase to one in the next. The CSO's recorded dissent would put no figure there (D-0011).
  • The US approved lane lists products from approvedStage: marketed first, then discontinued, then labels that rule the condition out, newest first within each.
  • On a person's search the lanes rise in sequence and totals count up over 600 ms; nothing animates on opening, on Back, or under reduced motion.

Console (views/console.tsx):

  • A figure strip: recruiting, completed, stopped, and trials that started since two calendar years before the year of the processing date (startedSince).
  • The phase by status matrix (matrix()): a cell, a row or a column is a selection (Pick), written as sel (encodePick, e.g. phase-2:recruiting, phase-2:any, any:stopped; any:any is never written).
  • The starts timeline from 1995 (FROM in console.tsx; the chart model's TIMELINE_FROM is 2000) with each US product's first approval pinned above the axis. Only an original approval from Drugs@FDA is pinned (SCI-247, display-dates.ts); indicated products dated by NDC marketing start, by a saved date of unrecorded type, with failed details or with no date are named in a note under the key instead (unmarkedNote). Pins closer than 24 px cluster (clusterPins); cluster centres are kept at least 24 px apart; a cluster opens an approvals list in the inspector. Labels that rule the condition out are not pinned.
  • Timeline by phase (Wave 4). A Segmented control, "Timeline shows: All trials / By phase" (one tab stop, arrow keys), writes tl=phase with replace history (the default all is omitted). By phase, each start-year column stacks aggregates.startPhases (start year × phase bucket × actual or anticipated) in PHASE_BUCKETS order; each bucket has a chart-slot colour and its own stroke pattern drawn over it in the surface colour (phase-marks.ts: none for Early phase 1, then diagonals, horizontals, verticals, dots, cross-hatch and grid), anticipated starts paler, with a phase key; phase is never encoded by colour alone. The data table gains a column per phase. A snapshot saved before Wave 4 has no startPhases, so "By phase" is disabled with "This saved snapshot does not keep start years by phase."

Registry coverage (Wave 4)

Under the condition's figures, on every view and width, parts/coverage-line.tsx shows "ClinicalTrials.gov only · phase recorded for {p} of {N} trials" (coverageCopy.line; p = coverage.phaseRecorded) and a "Coverage details" button that loads parts/coverage-details.tsx on demand. The details state the scope (ClinicalTrials.gov only; interventional; drug, biological or genetic interventions), that other registries (EU CTIS, ISRCTN, ChiCTR and the other WHO primary registries) are not searched and that US law does not require phase 1 drug trials to be registered (checked against 42 U.S.C. 282(j)(1)(A)(iii), Cornell LII, read 11 October 2026), analysed of total, the capped, partial or snapshot state, a field-completeness table in a focusable scroll region (phase, start date and primary completion date as recorded, anticipated or not recorded; enrolment not recorded), the lead sponsors' registry classes, the ClinicalTrials.gov API version and data date, and the standing qualification "Registered trials, not an asset inventory. Counts are trials, not drugs." A snapshot without coverage counts says so. The same lines go into the trials export's README.

Trials, Charts and US approvals

  • Trials (views/trials.tsx): the suite DataTable, 25 rows a page, default sort start date descending (DEFAULT_TRIAL_SORT), quick filter, column menu (hidden choices stored under storageKey(SLUG, "trials-columns")), row selection with "Copy NCT ids", phone cards below sm. Columns: row actions (not in the preview; one ⋯ key per row with Pin as its first item, ObjectActions pinInMenu: always visible, 1.5rem, 24 px before the 110 % scale, with a mouse, set by the row's own class [&_.ui-key]:[--key-h:1.5rem] so the shared component carries no option for it, and 44 px on touch; UI-27), NCT id, title (two lines, the sponsor on one beneath, the whole title as hover text; the CEO's feedback of 14 October 2026; it takes the free width, w-full, UI-27), phase, status, lead sponsor (hidden by default), start, enrolment (hidden by default, the CEO's feedback of 14 October 2026), modality, primary completion (hidden by default), industry. Estimated dates carry "~". Phase, status and start are sized to their content and never wrap (w-px whitespace-nowrap), modality stays on one line, and the pager is repeated above the table (pagerAtHead) as well as below it (UI-27, the CPO's ruling of 17 October 2026; e2e/drug-pipeline-explorer/trial-table.spec.ts).
  • Charts (views/charts.tsx, model chart-model.tsx): modality mix over time, median duration by phase, top sponsors, reasons for stopping. Phase by status and start years are the Console's and are not repeated. Each card has a legend, a "How this is counted" note, a data table and a provenance chip. Pressing a mark sets the cross-filter (live results only).
  • US approvals (views/approvals.tsx, helpers in approvals-panel.tsx): the products whose current label names the condition, with a headline sentence, caveats, tags in words ("Label says not for this condition", "Discontinued (Drugs@FDA)") and, in the inspector, links into Molecule for small-molecule ingredients.

Cross-filter (xf)

xfilter.ts defines one filter at a time, written xf=<dimension>:<key>:

Dimension Pressed in Key A trial matches when
status phase by status status group statusGroup(status) equals the key
modality modality over time modality group modality.lead equals the key
year start-year timeline YYYY start.year equals it
phase durations phase slug phaseSlug(phase) equals it
sponsor top sponsors company name canonicalSponsor(sponsor) equals it
stop reasons for stopping stop reason status group is stopped and matchWhyStopped(...).reason equals it
stage Board "All ... trials" p1 to p4 phase is in the stage
cell matrix "Open all ... in Trials" phase:status phase and status group both match

The base is the analysed trials minus imaging-only trials, so each subset count equals the pressed mark's value (xfilter.test.ts checks every dimension against the fixture). A filter that matches nothing in the current result is ignored. A second press on the same mark clears it. Snapshots carry only 100 trials, so the cross-filter works on live results only.

Inspector

inspector.tsx shows one Inspect target (store.ts): a trial (its record, its stage in context, , and . stepping), a US product (ProductRecord, Molecule links), a precedent product (PrecedentPanel, loaded with the precedent chunk), an approvals cluster, a matrix selection (count, shares, position in its row or column, newest trials eight at a time), the condition (when the inspector is opened with nothing picked) or the filters form. The inspector docks only where main keeps 60rem (DOCK_MIN_MAIN), so a pick never reflows the five Board lanes; otherwise it opens as a drawer or bottom sheet. Opening a trial, product or precedent product writes nct, prod or app; closing the inspector removes them and returns focus to the opener when focus went with the inspector.

FDA precedent view

views/precedent.tsx is imported with next/dynamic and carries data-chunk="pipeline-precedent". When it opens it fetches four snapshots (usePrecedentFiles) and makes one live letter count per session (useLetterCheck).

On a reference page whose pinned capture carries approvals (today only the complete response letter page, REL-217), usePipeline starts the approvals as { kind: "ready", pinned: true, snapshotDate } from the capture (referenceApprovals) instead of unsearchable, so nothing calls openFDA's label search. With pinned set the view and the product inspector skip the live letter count (and so the newer-letter files), the view shows a note naming the capture date, the label data date and the CRL export date ([data-precedent-pinned], copy precedent.pinned), the US approvals caveats say the list is from the reference capture (approvals.pinned), a page whose wording verdicts were re-checked after its capture says so in the view ([data-precedent-recheck], precedent.recheck) and in the caveats (approvals.recheck), and the Drugs@FDA marks and the panel chip cite openFDA as a snapshot with the capture date (drugsFdaSource(savedOn)). The same snapshot provenance applies when a live search fell back to a preset snapshot. Opening a product still reads its label's Clinical Studies section live (useLabelTrials). Its layout:

  1. Header, lede, a chip naming every snapshot with its date, a notice when the link named view=odds, and a stale notice when the compilation snapshot is more than 90 days old.
  2. Three KPI tiles over the rows the filters leave: median review time (shown only when n is at least 5; for 1 to 4 values the tile reads "Not computed" and its foot gives the rule and the values, "Median not computed: fewer than five products have both dates (n = …). Their review times: … days.", the CSO's ruling of 14 October 2026, never a dash; the footnote gives months, n, the year span and the priority and standard medians, and counts voucher reviews apart), the accelerated-approval share, and the expedited-designation share with the orphan share in its footnote.
  3. A filter panel (pathway, designations, review-time range in days, "with a published complete response letter"), written to pw, des, rt, crl.
  4. The table: Product, Pathway, Review time, Review, Breakthrough, Fast track, Orphan, CRL, Received, Approved, Application, and hidden-by-default Sponsor and Modality. Every value carries a ProvenanceMark. Below sm rows become cards with a "More about {brand}" disclosure.
  5. Notes on products left out (labels that only rule the condition out) and the unclassified share.
  6. The not-regulatory-advice and not-forecast statements, always in view, then a closed disclosure, "What these figures cover and leave out" ([data-precedent-limits]), holding the registry limits whose text matches /precedent|complete response|review time|designation|Clinical Studies/i (the CSO's ruling of 14 October 2026; the shell's limits notice and drawer still list every limit).

The inspector's ProductPrecedent shows the application (links to Drugs@FDA overview pages on www.accessdata.fda.gov and to FDA's approval letter when Drugs@FDA lists one), applicant, sponsor, approval date (and a note when the compilation's and Drugs@FDA's original approval dates differ), how the join was made, pathway and designations, breakthrough list rows, the review time with receipt and approval dates or the reason it is missing, joined complete response letters with the interval to approval and any rescission, letters published after the snapshot, the trials the label's Clinical Studies section names, and a link back to the product's US approvals record.

The e2e test bans the words odds, probability, likelihood, predict(s/ed/ing/ion), pivotal and validated from the view's text (BARRED in e2e/drug-pipeline-explorer/precedent.spec.ts).

State in the address

State lives in the #s= fragment, read and written only through useUrlState(pipelineSchema) (url-schema.ts; the mechanism is in Storage and state). The condition is the subject: a new condition pushes a history entry, everything else replaces. Defaults are left out, except lv once a result is on screen.

Key Meaning Check
condition Search text Hand-off text check (at most 120 characters), then cleanCondition, not empty
phase Comma list of EARLY_PHASE1, PHASE1 to PHASE4 checks.listOf
status Comma list of the nine status codes checks.listOf
sponsor industry or other checks.oneOf
from, to Start years integers 1980 to current year + 10; from > to drops both
view landscape (default), trials, charts, approvals, precedent; odds is aliased to precedent custom
lv board or console checks.oneOf
tl Timeline mode: all (default, omitted) or phase (Wave 4) checks.oneOf
sel Matrix pick parsePick, never any:any
nct Open trial ^NCT\d{8}$
prod Open US product `^(NDA
xf Cross-filter parseXf
pw Precedent pathway: nda, bla, accelerated, traditional canonical order, no repeats
des Precedent designations: breakthrough, priority, fast-track, orphan canonical order
rt Precedent review time in days, from-to, either end open, up to 5 digits each REVIEW_RANGE
crl Precedent: only rows with a joined letter checks.flag()
app Open precedent product same pattern as prod
mine A saved research view (mine: ref) HANDOFF_CHECKS.mine; never copied into a share link

Invalid values are dropped with a reason and reported in the shell's arrival strip, never as an error. The legacy query (?condition=&phase=&status=&sponsor=&from=&to=) is accepted and rewritten. filters.ts still holds the legacy parameter readers and writers (paramsToFilters, filtersToParams).

The progress record notes that the CTO and CPO advised prod rather than app for the precedent key, because app names the app slug elsewhere in the link contracts; the CEO's choice app was built (docs/progress-precedent.md, flag 3).

Per-device conveniences in browser storage (all through readLocal/writeLocal, which tolerate failure): Board/Console choice, card order, hidden trial columns.

Data flow

All live calls go straight from the browser to clinicaltrials.gov and api.fda.gov, the two hosts in the registry's dataPolicy.endpoints; the Content Security Policy is generated from that list (Security and network). fetchers.ts uses plain fetch with its own timeouts rather than the suite's client network layer, because the same code runs in the Node snapshot scripts.

fetchJson and request behaviour

  • Default timeout 20 s; an outer AbortSignal is honoured; errors are typed FetchError with kind timeout, network, http, aborted or parse.
  • notFoundAsNull turns HTTP 404 into null (openFDA's answer to a search with no matches).
  • retries repeats a request after 500 to 800 ms for network errors, timeouts and HTTP 429, 500, 502, 503 and 504.
  • mapSettled(items, limit, fn) runs at most limit calls at once and keeps every outcome.

ClinicalTrials.gov

studiesUrl(condition, filters, pageToken?) builds https://clinicaltrials.gov/api/v2/studies with:

Parameter Value
format json
query.cond cleanCondition(condition)
filter.advanced AREA[StudyType]INTERVENTIONAL AND (AREA[InterventionType]DRUG OR AREA[InterventionType]BIOLOGICAL OR AREA[InterventionType]GENETIC), then (AREA[Phase]X OR ...), AREA[LeadSponsorClass]INDUSTRY or NOT AREA[LeadSponsorClass]INDUSTRY, AREA[StartDate]RANGE[YYYY-01-01,YYYY-12-31] (MIN/MAX for an open end), joined with AND
filter.overallStatus `
fields The 15 STUDY_FIELDS: NCTId, BriefTitle, OverallStatus, WhyStopped, StartDate, StartDateType, PrimaryCompletionDate, PrimaryCompletionDateType, LeadSponsorName, LeadSponsorClass, Phase, EnrollmentCount, InterventionType, InterventionName, InterventionOtherName
sort StartDate:desc, so a capped search keeps the newest trials
pageSize 1,000
countTotal true on the first page; later pages repeat the parameters and add pageToken

The code comment records that parameter names, AREA[] fields and enum values were checked against the live API (OpenAPI version 2.0.5) in September 2026.

cleanCondition straightens look-alike apostrophes, removes quotes, brackets, backslash, caret and tilde (an unbalanced one makes the API reject the search), collapses spaces, cuts to 120 characters and passes the result through canonicalCondition (src/lib/search/conditions.ts): when the text exactly and unambiguously matches a Disease Ontology alias in the condition catalogue, the catalogue's preferred label is searched instead. Before the catalogue loads, the text passes unchanged; the first search from a link and any typed search wait for the catalogue to settle (loadCatalogs in use-pipeline.ts), then run under the canonical name, or as typed if the catalogue failed.

fetchTrials pages up to MAX_PAGES = 10 (10,000 trials). If the first page fails, the search fails. If a later page fails, the trials so far are returned with partialError and the failed nextToken, and "Try again" resumes from that page. capped is true when the cap stopped the download with a token remaining. fetchVersion reads https://clinicaltrials.gov/api/v2/version for apiVersion and dataTimestamp, the processing date the app shows.

Other ClinicalTrials.gov calls: studyUrl(nctId) for a trial's object tab (fetchStudy, 404 is "no such trial"), and ctgovIdsUrl (filter.ids, fields=NCTId) to check the NCT numbers a label names.

openFDA: the US approvals procedure

fetchApprovals(condition, options) in fetchers.ts:

  1. labelSearchFor(condition) builds the label search (see label search construction); undefined means "unsearchable" and no request is made.
  2. Applications: three drug/label.json count requests, one per prefix (NDA, BLA, ANDA), each search=(<label search>) AND openfda.application_number:<prefix>*, count=openfda.application_number.exact, limit=1000. Counting by prefix stops generics crowding brand-name applications out of the 1,000-term cap. If all three return nothing and the search has a word-by-word fallback, the three are repeated with it. NDA and BLA numbers become the application list; ANDA terms are only counted (generics). A count that returns 1,000 terms is flagged truncated.
  3. Greek letters: when the condition names one (withoutLetter), the NDA and BLA counts are repeated for the other words. If the extra applications number at most LETTER_BUDGET = 12, their labels are read in chunks of six (two at a time, paged 100 per request) and an application counts only when a sentence of its indications matches the original search (letterMatches). More than 12 are reported as unchecked.
  4. Unlinked labels: one request counts matching labels with no openfda block (_missing_:openfda). In the browser they are only counted. With resolveUnlinked (the snapshot script only) every such label is downloaded (50 a page), the product NDCs printed on its carton panel or How Supplied section are extracted, looked up in drug/ndc.json in chunks of 50, and mapped to NDA or BLA applications. A label whose NDCs lead to more than one generic name (a kit) is attributed to none. When a fresh preset snapshot exists, the live lookup reuses its traced applications (knownFromSnapshot).
  5. Details: drug/drugsfda.json for applications in chunks of 40 (three at a time), then drug/ndc.json for applications Drugs@FDA lacks. A chunk that fails leaves its applications notLoaded, listed without brand or date.
  6. Label wording: one count request finds applications whose matching labels contain a negative phrase (12 phrases, NEGATION_PHRASES). Up to wordingBudget (12 in the browser, unlimited in the snapshot script) of those not already checked by the snapshot have their labels read (chunks of six) and are flagged negated when every matching sentence rules the condition out.
  7. buildApprovals groups and sorts the products (see product grouping).

The registry publishes a ceiling of 12 openFDA requests per condition for this procedure (PRECEDENT_REQUESTS.approvalsCeiling), measured weekly on the seven examples by pipeline/drug-pipeline-explorer/precedent/verify.ts. It is a measured figure for the examples, not an enforced limit: a free-text condition with many applications makes more requests (one Drugs@FDA request per 40 applications, for instance).

parseDrugsFda reads, per application: brand names and active ingredients of every product, sponsor_name, the earliest ORIG submission with status AP and a YYYYMMDD date (the original approval), its review_priority, submission_class_code_description and the URL of its Letter document (rewritten from http: to https:), and discontinued when every product's marketing_status is "Discontinued". parseNdc reads brand, generic name, labeler and the earliest marketing_start_date; a product is discontinued when its marketing_end_date is before today (wall clock at parse time).

Example snapshots and the fallback

use-pipeline.ts holds the flow:

  • After 7 s without the first page (SLOW_AFTER_MS), an example search offers its saved snapshot if one exists and is not stale; the person can switch to it ("ClinicalTrials.gov was slow to respond").
  • If the live search fails (except HTTP 400, a malformed query) and the search is an example with default filters, the snapshot is shown automatically. With filters set, the snapshot is offered but never substituted silently; the status says the snapshot ignores filters.
  • A snapshot more than 60 days old is never shown (SNAPSHOT_MAX_AGE_DAYS, snapshotIsStale; an unreadable date counts as stale). Without a usable snapshot the failure is shown with its cause.
  • If openFDA fails for an example, the snapshot's approvals are shown with their date (snapshotDate).

Finished searches and approvals are kept in memory for 30 minutes, at most 10 entries each (CACHE_TTL_MS, CACHE_SIZE), keyed by the studies URL and by the lower-cased condition. Partial results and approvals with unresolved applications are not cached. Each snapshot file is requested once per page load.

FDA precedent loads and live calls

Call When Count Code
/data/drug-pipeline-explorer/precedent/{cder-nme,breakthrough-approvals,crl,project-confirm}.json View or inspector panel first opens 4 per session (retry once each) loadFiles
https://api.fda.gov/transparency/crl.json?count=application_number&limit=1000 View opens 1 per session; no search term, no letter fields runCheck
transparency/crl.json?search=application_number:"NDA 209531"&count=file_name&limit=100 Opening a product whose live letter count exceeds the snapshot's 1 per such application useNewerLetterFiles
drug/label.json?search=openfda.application_number:("..." OR ...)&limit=20 Opening a product in the precedent inspector 1 per product per session loadLabel
ClinicalTrials.gov filter.ids check After the label, when it names NCT numbers 1 per product loadLabel

These budgets are published in the registry (PRECEDENT_REQUESTS) and counted in e2e/drug-pipeline-explorer/precedent.spec.ts: no precedent file or letter request before the view opens, four file requests and one letter count when it does, nothing again on return.

Object tab fetches

The condition tab runs usePipeline for the record's words with default filters (with the same snapshot fallback). The trial tab calls fetchStudy(nctId). The product tab calls fetchProduct(applications): Drugs@FDA, then the NDC directory for the rest, grouped as in the approvals list.

Shipped data files

Example snapshots

public/data/drug-pipeline-explorer/snapshots/<id>.json (type Snapshot, version: 1) holds retrievedAt, the ctgov version block, totalCount, analysed, capped, the full aggregates, the 100 most recently started trials (recentTrials, SNAPSHOT_TRIALS), the full approvals result (including ndcLinked, unlinked and wording from the heavy steps), the source URL and a terms block with ClinicalTrials.gov attribution, terms URL and the site's modification statement, and openFDA's attribution and licence. index.json lists each file with its totalCount and exact byte size. Floats are rounded to three decimals.

The files shipped now were retrieved 2026-10-17T15:49:45Z under wording rule 3 (REL-221), ClinicalTrials.gov dataTimestamp 2026-10-16T09:00:05, API 2.0.5:

Preset Matching trials Analysed US products (labels) Negated Patient group Unlinked labels (traced)
Alzheimer disease 1,734 1,734 14 0 0 276 (7)
Obesity 3,091 3,091 15 5 0 425 (2)
Non-small cell lung cancer 6,276 6,276 72 0 0 136 (10)
Type 2 diabetes 5,418 5,418 63 3 6 2,473 (32)
Rheumatoid arthritis 1,889 1,889 72 1 0 3,322 (73)
Duchenne muscular dystrophy 268 268 8 0 0 5 (2)
Sickle cell disease 582 582 10 0 0 5 (3)

None is capped. They are produced by pipeline/drug-pipeline-explorer/snapshot.ts, which runs the app's own fetchTrials (60 s timeout, 3 retries), aggregate and fetchApprovals (resolveUnlinked: true, unlimited wording budget, concurrency 2, 3 retries), and stops rather than write a snapshot with a partial trial download or unresolved openFDA details. Refresh, expiry and the CI checks are in Data pipeline.

The snapshots are also read by scripts/build-search-catalogs.mjs, which takes brand-name aliases for the compound catalogue from the snapshots' approvals (a brand maps to a compound only through an exact single ingredient name).

FDA precedent snapshots

Five files under public/data/drug-pipeline-explorer/precedent/, each with the envelope schemaVersion: 1, id, title, publisher, sourceUrl, licence, licenceReadOn, retrievedOn, files (URL, file name, SHA-256, bytes, last-modified, content type, dataAsOf), extractedBy and rows. All five were retrieved and their licence positions read on 2026-10-05.

File Script Source Rows Used by the app
cder-nme.json fetch-cder-nme.py (openpyxl) FDA, Compilation of CDER NME and New Biologic Approvals 1985-2025 (XLSX, sheet "2025 Compilation", https://www.fda.gov/media/177921/download), with dictionaryUrl and coverage (first approval 1985-04-09, last 2025-12-30) 1,387 (279 BLA; 26 with the NDA-to-BLA transition note; 7 relicensed; 19 voucher reviews) Yes
breakthrough-approvals.json fetch-breakthrough.py (pypdf) CDER Breakthrough Therapy Approvals PDFs: current year (data as of 2026-06-30, 15 rows) and prior years (to 2025-12-31, 337 rows) 352 (203 ORIGINAL) Yes
crl.json snapshot-crl.mts openFDA transparency/crl bulk zip (export 2026-10-02), reduced by stripLetter 457 of 458 records (one "Under Review for Release" skipped); letters dated 2002-03-15 to 2026-08-07 Yes
project-confirm.json fetch-project-confirm.mts FDA Oncology Center of Excellence, Project Confirm, four HTML tables 239 (ongoing 56, verified 127, withdrawn 36, other 20) Loaded, not displayed
surrogate-endpoints.json fetch-surrogate.py (openpyxl) FDA Table of Surrogate Endpoints (XLSX, four sheets) 225 Not loaded

The compilation script checks the 27-column header exactly, requires each flag to match Yes|No|N/A with FDA's lettered-indication qualifiers, the review designation to match Priority|Standard with qualifiers, the approval year to equal the approval date's year, and the receipt not to follow approval; any surprise stops the run. It keeps the indication text only on rows whose flags name a lettered indication, and checks but does not ship the QIDP and voucher-issued columns. The breakthrough script checks each PDF's "Data as of" date and that the parsed row count equals its "Total of N Approvals". The CRL script reads the zip in memory, never writes it to disk, and records the zip's SHA-256.

Science and analytics

Phase buckets, stages and statuses

phaseBucket(phases) (ctgov.ts), first rule that applies: PHASE2 and PHASE3 gives "Phase 2/3"; PHASE1 and PHASE2 gives "Phase 1/2"; then PHASE3, PHASE2, PHASE1, EARLY_PHASE1, PHASE4 alone; otherwise "Not applicable". The Board's stages are in Landscape.

Status groups (statusGroup in aggregate.ts): recruiting (not yet recruiting, recruiting, enrolling by invitation); active (active, not recruiting); completed; stopped (terminated, withdrawn, suspended); unknown (ClinicalTrials.gov's UNKNOWN, any unrecognised value is read as UNKNOWN).

Registry dates

parsePartialDate accepts YYYY, YYYY-MM or YYYY-MM-DD (month 1 to 12, day 1 to 31). dayNumber converts to a UTC day: a month-only date is the 15th, a year-only date 1 July. startEstimated and primaryCompletionEstimated are true when the date's type is ESTIMATED.

Aggregations (aggregate.ts)

aggregate(trials) runs once over every analysed trial. Trials whose modality is diagnosticOnly (every drug they name is a diagnostic or imaging agent) are counted in imagingOnly and left out of everything else.

Output Rule
summary Counts per status group, and industry-led trials (LeadSponsorClass INDUSTRY)
phaseStatus Phase bucket by status group; empty phases dropped
modalityByPeriod Lead modality by start-year period: before 2005, 2005 to 2009, 2010 to 2014, 2015 to 2019, 2020 to 2024, 2025 and later; undated trials excluded
modalityLead, kindCounts, modalityCoverage Trials per lead group; trials per fine kind (a trial counts once per kind); interventions considered and classified
startYears, undated Per start year, started (actual date) and planned (estimated date)
sponsors, sponsorCount By canonicalSponsor, "Not stated" excluded, sorted by count then name; top 12 kept (TOP_SPONSORS); registered lists the merged names when more than one. The company's industry flag is taken from the first trial seen for it
durations, durationMethod For COMPLETED trials only: missing start or primary completion, or either date estimated, excludes the trial (counted); days = dayNumber(primary completion) - dayNumber(start); non-positive excluded (counted); years = days / 365.25; per phase bucket the median and the 25th and 75th percentiles
stopReasons, stoppedTotal For the stopped group: counts per stop reason and the clearest example text (a stated reason that matches only its own group, then one matching several groups, then a consequence)
coverage (Wave 4) coverageOf(trials) over every analysed trial, imaging-only ones included: phaseRecorded (phase bucket other than "Not applicable", which covers the registry's NA and no phase); start dates actual, anticipated (ESTIMATED) or missing; primary completion dates the same; enrolmentMissing; sponsorClasses, a count per raw LeadSponsorClass (NOT_STATED when absent)
startPhases (Wave 4) Per start year and phase bucket, started and planned, for counted (non-imaging) dated trials, sorted by year then bucket order
figureSponsors, individualTrials (Wave 4) For the sponsor figure: trials per canonicalSponsor company with "Not stated" and individual-class (INDIV) lead sponsors left out, top 12; individual-class trials counted together, never named

The four Wave 4 fields are optional on Aggregates, so snapshots saved before Wave 4 lack them and the by-phase timeline and the figures are disabled with a reason. Trial.sponsorClass (new, optional) keeps the registry's LeadSponsorClass as given (INDUSTRY, NIH, FED, OTHER_GOV, INDIV, NETWORK, OTHER, AMBIG, UNKNOWN; "" when absent); parseStudy already requested the field for industry.

Percentiles use linear interpolation between order statistics (R type 7, quantile). Durations are single-trial durations, not development times, and exclude trials that stopped early (registry limits).

Headline figures (landscape-model.ts headline): counted is the sum of the five status groups; open is recruiting plus active; industryShare is industry-led over counted; partial is analysed below total. Shares display as whole percent, with "<1%" for a share above zero and below 0.5 percent.

Modality classifier (modality.ts)

The classifier assigns each trial a set of modality kinds from intervention names. It never guesses from company codes and never uses the registry's GENETIC type alone (it also covers genetic tests).

Kinds and groups:

Group Kinds
Small molecule small-molecule
Monoclonal antibody mab
Antibody-drug conjugate adc
Peptide peptide
Other protein protein
Vaccine vaccine
RNA or oligonucleotide sirna, antisense, aptamer, mrna
Cell or gene therapy car-t, cell, gene
Diagnostic or imaging agent diagnostic
Unclassified none matched

classifyName(name), in order:

  1. Lower-case, drop apostrophes. If the name matches PLACEBO (placebo, sham, vehicle, saline alone, dummy): a name that starts with the placebo word is a placebo; otherwise the name is split into arms at /, +, ,, ;, "or", "and", "plus", "vs", "versus" (brackets removed) and only non-placebo arms are kept ("semaglutide or placebo" keeps semaglutide; "donanemab placebo" is a placebo).
  2. Keywords, first match wins: diagnostic (radiolabels such as [18F], F-18, C-11, Ga-68, Tc-99m, Cu-64, Zr-89, I-123/124, In-111, and the words PET, SPECT, tracer, imaging agent, contrast agent, FDG, Pittsburgh compound, PiB, DaTscan; therapeutic isotopes such as lutetium-177 and iodine-131 are not in the list); CAR-T; vaccine or toxoid; antibody-drug conjugate or "ADC"; siRNA or RNAi; antisense or "ASO"; mRNA; gene therapy, gene transfer, AAV, adeno-associated, lentivirus; cell therapy patterns (a -cel word, stem cells, T or NK cells, TILs, mesenchymal, MSCs, CD34, autologous or allogeneic cells), skipped for blood products and for exosomes, vesicles and engagers; "monoclonal antibod"; protein words (insulin, interferon, immune globulin, immunoglobulin, fusion protein, enzyme replacement, anti-thymocyte, ATG); "peptide". Blood products (red cells, platelets, whole blood, transfusion, plasma) return no kind.
  3. Aptamers: pegaptanib by name, or a word containing "apt" (not at its start) beside the word "pegol".
  4. Named phrase: "vanutide cridificar" is a vaccine.
  5. Pairs: an antibody-stem word followed by an ADC payload-stem word (-dotin, -tansine, -tecan, -irine, -micin) is an ADC. A -ment word (antibody fragment, 28 English words excluded) directly before an oligonucleotide or peptide word (-rsen, -siran, -meran, -tide) adds mab; the partner word is still classified.
  6. Each remaining word: first the named drugs (NAMED_DRUGS: peptide hormones such as corticotropin, glucagon, calcitonin, leuprolide, bivalirudin; protein hormones such as somatropin and the gonadotropins, ignored when followed by "releasing"; polyclonal globulins; approved tracers florbetapir, florbetaben, flutemetamol, flortaucipir, ioflupane, tilmanocept, iodixanol; azacitidine as a small molecule). Then matchWord: prefix stems first (cef-, gli, sulfa-, vin-, pred, gado-, io-), then "prednis" inside a word of at least 12 letters, then suffix stems longest first (137 rules), then infix stems (-grel-, cort). -ase is ignored when the name also says inhibitor, antagonist, agonist, activator, modulator, blocker or degrader; io- is ignored when the name carries iodine-131.
  7. If both cell and gene were found, gene is dropped (a gene-modified cell product, such as exagamglogene autotemcel, is a cell therapy in the INN scheme).

Each stem rule has a minLength (shorter words never match), an exclude list of whole words (for example "cholesterol" for -terol, "cellcept" for -cept, "thermostat" for -stat) and optionally an excludeEnding (globulin for -bulin). Without minLength the minimum is the stem length plus three. Words are tokenised on non-alphanumerics and hyphens, except that a token ending -cel is kept whole.

classifyIntervention considers only types DRUG, BIOLOGICAL, GENETIC and COMBINATION_PRODUCT (or no type). It classifies the name, then each of the registry's other names, and stops at the first that yields a kind. classifyTrial combines a trial's interventions: groups in LEAD_PRIORITY order (cell or gene, nucleic acid, vaccine, ADC, antibody, peptide, protein, small molecule), with diagnostic last; the lead group is the first in that order, or diagnostic when every considered intervention is diagnostic-only. The lead convention is documented as a convention, not a statement of which agent is under test. basis holds human-readable reasons for tooltips.

The code holds 151 distinct stem strings (counted by the same regular expression verify_stems.py uses; -tecan and -micin each appear twice, as an ADC payload and as a small-molecule stem). Each is cited to WHO, "Use of stems in the selection of International Nonproprietary Names (INN) for pharmaceutical substances, 2024", Part II B. pipeline/drug-pipeline-explorer/verify_stems.py reads Part II B of the stem book PDF and fails if any stem is not listed there; it prints the book's definition beside each so the short class labels (the project's own) can be compared by eye. The stem book is CC BY-NC-SA 3.0 IGO and is used only as a reference (sourceMeta["who-stems"].displayOnly); its definitions are not shipped. Stems that collide with English endings (-ine, -one, -ium, -ast) are not used.

The classifier is not verified against an independent labelled dataset. Its unit tests (modality.test.ts) are hand-written cases. The precedent brief's proposed precision fixture for modality tags (95 percent on classified rows, docs/design/brief-precedent.md §4.1) does not exist.

Stop-reason grouping (why-stopped.ts)

Groups, in display order: safety, efficacy, COVID-19, enrolment, business, funding, regulatory, supply, site, other, not stated.

matchWhyStopped(text):

  1. Empty text is "not stated".
  2. prepare: lower-case; remove spans that deny a safety, efficacy, toxicity, tolerability, adverse, futility or concern reason ("not due to safety", "no safety concerns", "unrelated to", "non-safety", "safety profile unchanged"); remove non-causal safety mentions ("pre-planned analysis of safety and efficacy", "long-term safety follow-up"); rewrite a "data safety monitoring board" to "monitoring board".
  3. Test the rules in order: a monitoring board stopping a trial "due to concerns" not named as something else (safety); safety words; efficacy and futility words; COVID; enrolment phrases; inability to enrol; business and strategy; funding; regulator or ethics; drug supply; site and logistics; then consequences ("no participants enrolled"), which file under enrolment only when nothing else matched.
  4. If the first match is enrolment (not a consequence), statedCause looks for a funding, regulatory, supply or site cause given for it (after "due to", "because of", before ", so", "therefore", in "X reasons", or something missing before the inability) and files the text under that cause instead.
  5. No rule matched: "other".

The code comments cite registry examples by NCT id for the edge cases (NCT03743025, NCT03786432, NCT04241588, NCT00559286, NCT01137448, NCT01416948).

31 rules, each anchored at the start of the name and followed by a space, comma, full stop or the end, merge a company's registered variants (for example "Pfizer", "Wyeth" and "Wyeth is now a wholly owned subsidiary of Pfizer" become "Pfizer"; "MSD" and "Merck Sharp & Dohme" become "Merck Sharp & Dohme (MSD)"). Separately registered companies that a group owns are never merged (Genentech stays apart from Roche, Celgene from Bristol Myers Squibb). The sponsor chart, the sponsor cross-filter and the export's sponsor_company column use the merged name; the trial list and the inspector keep the registered name. Object facts never record a sponsor's name, because a lead sponsor can be a person (objects.ts).

Label search construction

labelSearchFor(condition) (openfda.ts):

  • An example uses its hand-checked query (presets.ts), for example indications_and_usage:alzheimer* or indications_and_usage:("non-small cell lung cancer" OR "non-small-cell lung cancer" OR NSCLC).
  • Otherwise capital AND, OR and NOT are removed; text is folded (accents removed, ligatures expanded, lone Greek letters spelled out, apostrophes straightened). Text that still has non-ASCII letters after folding is unsearchable (hasForeignLetters).
  • Words of fewer than three letters, stopwords and leading or trailing hyphens are dropped; a search made only of generic words (disease, syndrome, disorder, chronic, acute, severe, type, patients and others) is unsearchable.
  • A single word or a possessive gives a word search: each word as a prefix (word*), hyphenated or numbered words quoted, joined with AND. Otherwise the exact phrase is searched, with the word search as fallback.
  • When a Greek letter name is present, withoutLetter holds the search without it.

conditionMatcher turns one of these searches back into a sentence test (phrases, prefixes, words, AND or OR). sentences splits label text at sentence ends, bullets, line breaks, before "Limitations of Use" and after a section reference such as "( 1 , 14.3 )".

Label wording (negation)

A label is negated for the condition when at least one sentence of its Indications and Usage section mentions the condition and every such sentence matches NEGATED. It is population (rule 3, REL-221) when its positive sentences all name the condition only as the patient group of another use; otherwise it is indicated. One verdict from several labels, applications or a product's applications takes the strongest (strongestMention: indicated over population over negated). Only indicated counts a product as one for the condition: negated and population products stay in the US approvals list with their own tags ("Label says not for this condition", danger colour; "Label names this condition only as the patient group", neutral, "Patient group only" on Board cards and inspector records), are excluded from the Board's pins, the marketed count and the timeline (the approved lane counts "US labels" and splits them into marketed, discontinued and not indicated, so marketed plus discontinued is the header's US products; the CPO's memo 2026-10-17-cpo-export-verdict-and-lane-label.md), get no Molecule names (workbenchNames is filled only for indicated rows), and are left out of FDA precedent, each counted on its own line ("ruled out"; "approved for another use whose label names {condition} only as the patient group: not counted"). They sort after marketed and discontinued products, population before negated (approvalRank; the CPO's memo docs/board/memos/2026-10-17-cpo-population-verdict-and-note-cap.md).

Rule 3's population test (populationTest(search); the CSO's memo docs/board/memos/2026-10-17-cso-population-verdict.md) has two forms. The efficacy form, "(established|studied|evaluated|demonstrated) in a population with", runs for every condition. The cardiovascular form, "reduce the risk of … (myocardial infarction, MI, stroke, cardiovascular, heart failure, revascularization, angina, MACE, coronary) … with", runs only for the searches in POPULATION_CV_SCOPE: type 2 diabetes today. For atrial fibrillation, heart failure, chronic kidney disease, coronary disease or stroke the same grammar is a genuine indication, so they are never added; each addition needs its own scan and the CSO's review. openFDA flattens indication lists into one sentence, so a sentence is a population sentence only when every indication clause in it that names the condition is one; clauses split where a full stop is followed by a lower-case "to", "as" or "for" (OZEMPIC's glycaemic-control item and its MACE item). The rule is narrow on purpose (reversed order, "lower the risk", "reduce the incidence" and renal-only outcomes are not recognised; REL-223), erring towards "indicated". On the seven presets' labels (17 October 2026, including the NDC-traced ones) it changes six type 2 diabetes applications to population: BRILINTA, INPEFA, ATORVALIQ, LIPITOR, CADUET (NDA021540) and KERENDIA, whose label has no openfda block.

NEGATED is wording rule 2 (WORDING_RULE; REL-220, the CSO's memo docs/board/memos/2026-10-17-cso-label-negation-rule.md). Rule 1's phrases stand unchanged: not indicated, recommended, approved or intended; not been established, demonstrated or shown; should not be used; no substantial evidence. Rule 2 adds not to be effective, not established, not shown to, did not reduce, improve, demonstrate or show, and do not use, and moves rule 1's "not (be) (expected to be) effective" in with them. These words also describe patients ("whose disease did not improve with one or more DMARDs", "for whom other treatments were not effective"), so they do not count after who, whose, whom, which or that earlier in the clause; the guard errs towards "indicated". On the current labels of all seven presets' products (421 labels, read 17 October 2026) rule 2 changes four applications, all to negated: SYPRINE (NDA019194) for rheumatoid arthritis, and TRICOR, LIPOFEN and ANTARA (NDA021656, NDA021612, NDA021695, fenofibrate) for type 2 diabetes. A label that names the condition as a population ("in adults with type 2 diabetes mellitus with multiple risk factors") is still an indication (REL-221).

The labels worth reading are found with two openFDA counts made in parallel: one over NEGATION_PHRASES, which holds every phrase NEGATED can match, and one over populationQuery(search) (populationCountUrl), which holds every word the population test can match for that search (a unit test checks both). At most wordingBudget (12) applications' labels are read live, all or none (the CTO's memo docs/board/memos/2026-10-17-cto-population-wording-budget.md: openFDA has no field selection, and the 22 type 2 diabetes population candidates' labels are about 15 MB). When over budget, wording.populationUnread counts the population candidates left unread; they stay listed as indicated, and the approvals caveats and the precedent list say how many (approvals.populationUnread, precedent.populationUnread), so the figures are not presented as clean. A live lookup reuses a preset snapshot's verdicts (knownFromSnapshot). Each ApprovalsResult.wording records the rule it was made under (rule, absent for rule 1). For a snapshot from an earlier rule, fetchApprovals reads again the applications a later rule may judge differently (reusableVerdicts): for a rule-1 snapshot, those a count over RULE_2_CHANGED_PHRASES finds (one more request; on the seven presets 0 to 9 applications); for a rule-1 or rule-2 snapshot, the population candidates. The rest of the snapshot's verdicts stand. The preset snapshots were regenerated under rule 3 on 17 October 2026, so a live preset search reuses all of them. If that count fails, the snapshot's verdicts all stand and the result records the earlier rule (wording.rule), as when openFDA is down and the list falls back to the snapshot; either way the caveats say the verdicts rest on an earlier version of the check (approvals.olderRule).

Display dates by source (SCI-247, 24 October 2026)

The CSO's ruling (docs/board/memos/2026-10-24-cso-pipeline-date-labels.md) and its case table pipeline/drug-pipeline-explorer/fixtures/display-date-semantics-v1.json, committed before the code, decide every date label. display-dates.ts classifies a product's selected date: O original approval (the earliest Drugs@FDA submissions[].submission_status_date with submission_type ORIG and submission_status AP, parseDrugsFda), M NDC marketing start (the earliest NDC Directory marketing_start_date, parseNdc), U a date saved without its type, N details not loaded (notLoaded, whatever date a capture carries) and X no date. Only O is called an approval ("First approved", "Original approval"), pinned on the timeline's approval axis or cited to Drugs@FDA. M reads "NDC start {month}" / "NDC marketing start", is cited to the NDC Directory and carries "not an approval date"; U reads "type not recorded"; N reads "Not loaded". A joined precedent row keeps the compilation's FDA Approval Date. Date selection and grouping are unchanged, so a mixed group whose earliest date is a marketing start reads as M even when originals holds a later approval (a recorded limit). Tested by display-dates.test.ts (14 cases, every expected string compared as written) and e2e/drug-pipeline-explorer/display-dates.spec.ts.

Product grouping and Molecule eligibility

buildApprovals merges applications with the same type, brand set, ingredient set and sponsor into one product; the earliest date wins; a product is discontinued or negated only if all its applications are. Applications whose details failed are never merged and carry notLoaded. Sort: marketed, then discontinued, then negated; newest date first; undated last. kinds come from classifying each ingredient name; a diagnostic ingredient makes the whole product diagnostic.

An NDA product is treated as a small molecule (inferredSmallMolecule) when it has ingredient names, none classifies as a biologic, peptide, oligonucleotide or diagnostic kind, and none is a known polymer or oligomer mixture (heparin, enoxaparin, dalteparin, glatiramer, colesevelam, sevelamer, patiromer, colestipol, cholestyramine, porfimer). BLAs never are. For an indicated small-molecule product, workbenchNames are the ingredients with salt words and radiolabel suffixes removed (parentName), which become the Molecule links.

FDA precedent

The rules come from the CSO ruling of 5 October 2026 recorded in docs/progress-precedent.md and in the module headers.

Join (nme.ts, JOIN_METHOD = { id: "appno-exact", version: "1.0.0" }). The application number is the only key; nothing joins by name. indexCompilation maps every application of every compilation row, then, from the rows' own notes, the NDA form of a BLA marked "Transition Product from NDA to BLA" (via: "converted") and the BLA named in "now referenced by BLA nnnnnn" (via: "relicensed"). joinProduct takes the earliest-approved row any of the product's applications leads to. normaliseApplication accepts NDA, BLA or BL, 4 to 6 digits, and pads to six.

Review time (review-time.ts, REVIEW_TIME_METHOD, review-time-receipt-approval 1.0.0). Days = FDA Approval Date minus FDA Receipt Date from the compilation row, in UTC calendar days, only when both exist and the difference is not negative; months = days / 30.4375. The receipt date is "the date the first and complete marketing application was received by FDA", so the figure includes any complete-response cycle. Nothing is ever estimated. When the unchanged calculation cannot return a nonnegative interval, joined rows receive one of the first two reasons below before any product fallback. The table and inspector use “Review time unavailable” for these joined cases; legacy unjoined headings and reasons remain unchanged:

Reason Rule (first that applies)
receipt-not-recorded compilation join exists but its receipt date is null or empty
invalid-review-interval compilation join exists, receipt is present, and the existing calculation cannot return a nonnegative interval
not-loaded openFDA details failed
cber dated only by NDC marketing start, or no date and no class code
after-coverage approval after the compilation's last approval (2025-12-30)
before-coverage approval before its first (1985-04-09)
not-joined Drugs@FDA class code starts "Type 1" (new molecular entity) but no row joined
not-in-compilation anything else, with no compilation join

When the nonempty base precedent table is filtered to zero rows, all three figure reasons and the median footnote say “No products match these filters”; this is a statement about the selected result, not FDA source absence. With rows still present, unavailable accelerated approval says “No products here have a recorded Yes or No accelerated-approval flag in FDA's compilation”, while unavailable expedited designation says “No joined products here were approved on or after 9 July 2012”. The existing median missing-date and fewer-than-five qualifications, null values, arithmetic, eligible denominators and method remain unchanged. Browser coverage reuses the shipped RIDAURA snapshot row to exercise nonempty zero denominators and the existing small-n interval without altering source evidence.

Designations (designations.ts). Compilation flags are parsed as Yes, No or N/A with an optional verbatim qualifier ("indication [A] only"). N/A reads "programme did not exist" (not-yet) only when the approval predates the programme: accelerated approval 1992-04-15, fast track 1997-11-21, breakthrough therapy 2012-07-09, priority review vouchers 2007-09-27 (PROGRAMME_START, from the compilation's data dictionary); any other N/A is shown as written; orphan has no start date. Anything else is unread and shown raw, never as "No". Review designation is Priority or Standard; "(used priority review voucher)" sets voucher. Products outside the compilation take Drugs@FDA's review_priority. FDA's breakthrough lists join exactly by application number; for a compilation product, the ORIGINAL- rows cross-check the flag (agree, disagree, shown as "sources disagree", or unchecked); for other products the lists give "on the list", "on the list for a later indication", "not on CDER's list as of {date}" or "programme did not exist".

Figures (precedentFigures). Over the rows passed in (the filtered table), counting each compilation row once and using joined rows only:

  • Median review time over computed rows (median of an even count is the mean of the middle two); priority and standard medians exclude voucher reviews; vouchers counted apart.
  • Accelerated share: Yes over Yes plus No.
  • Expedited designation share: among joined rows approved on or after 2012-07-09, those with fast track Yes, breakthrough Yes, accelerated Yes, or a priority review not bought with a voucher.
  • Orphan share: Yes over Yes plus No; shown as a footnote, apart from the expedited share.

The brief defines the KPI figures over the joined rows; the build computes them over the rows the filters leave and labels them "n of N" (docs/progress-precedent.md, flag 12).

Filters (filterRows). Pathway types (NDA, BLA) are ORed, routes (accelerated, traditional) are ORed, and the two groups are ANDed; a route filter matches only compilation rows whose flag is Yes (accelerated) or No (traditional). Designations match any of: breakthrough (compilation Yes, else list status "original"), priority (compilation, else Drugs@FDA), fast track and orphan (compilation Yes only). A review-time range excludes rows without a computed time. The CRL filter keeps rows with at least one joined letter.

Complete response letters (crl.ts, letters.ts). stripLetter copies only application_number, company_name, letter_date, letter_type, approver_center and file_name (PERMITTED_FIELDS); company_rep, approver_name, approver_title, company_address and text (FORBIDDEN_FIELDS) are never read. A record without a readable MM/DD/YYYY date, a .pdf file name or an NDA/BLA number is dropped. A letter joins a product when it is on an original application of the product (no supplement number), its letter_type is exactly "COMPLETE RESPONSE", and it is dated before that application's original approval in Drugs@FDA; the interval is approval minus letter date in days. A later "RESCIND COMPLETE RESPONSE" on the same application is shown beside it. Letters on supplements wait for phase B. Nothing about a letter is summarised or classified. The live check compares per-application counts from the one count call with the snapshot's counts (newerLetters) and fetches file names only for applications with more letters live.

Trials named in a label (pivotal.ts). NCT numbers are read with \bNCT\s?(\d{8})\b from the newest label version's clinical_studies and clinical_studies_table fields (newest by effective_time, then version), normalised, first mention first, without repeats. They are called "trials named in the label's Clinical Studies section", never pivotal. Each is checked against ClinicalTrials.gov; one it does not hold is shown as written, marked "not found on ClinicalTrials.gov" with the check date. Only the numbers are kept, never label text.

Project Confirm (confirm.ts, confirm-name-date 1.0.0, not shown). Project Confirm has no application numbers, so the join is by name and date: brand equal to one of the product's brands, ingredient set equal after normaliseName (accents and case folded, punctuation closed up, 58 salt words dropped after the first word only; isotopes, "liposome", ado-/fam- prefixes and biologic four-letter suffixes kept), and the accelerated-approval date equal to an original approval date the product carries. Measured on the 100-case names.json fixture: 29 shown, 0 wrong, 21 missed, 43 name-only; precision 1.000, Wilson 95 percent lower bound 0.883, recall 0.580 (printed by confirm.test.ts). The CSO threshold is precision at or above 98 percent; 29 matches cannot put the lower bound there (about 188 would), and the fixture's truth uses the same dates as the rule. Showing it is the CEO's decision; nothing in the app imports the module.

Surrogate endpoints. Snapshotted and checked weekly, not loaded or shown (by disease, not product).

Provenance and limits

  • Successful non-aborted live openFDA acquisition records retrievedAt once in ApprovalsState and its in-memory cache. Cache hits, rendering, saving and exports retain that date; a new acquisition receives a new date. Product export provenance and learned product/condition facts use the same acquisition date. Snapshot results retain their published snapshotDate; legacy states without either date retain unknown acquisition time (no first-seen or export-time substitute). openFDA lastUpdated is the API data date, while the export block creation time is when the file was generated.
  • provenance.ts builds the ClinicalTrials.gov chip: live results cite the studies URL, apiVersion, the processing date (or "processing date unavailable") and the retrieval date; snapshots cite their retrieval date and expiry (retrieval plus 60 days). approvalsProvenance cites openFDA with meta.last_updated.
  • precedent-provenance.ts gives every precedent value a claim-level citation: compilation cells cite the sheet, row number and column (factId cder-nme:<row>:<column>); the review time is a measured claim with REVIEW_TIME_METHOD; breakthrough cells cite the list rows and the lists' date; Drugs@FDA cells cite the applications; letter cells cite each letter's date, type and URL. The panel chip names the compilation (with the first 12 characters of its SHA-256), the lists' date, openFDA's letter last_updated and each snapshot's own retrieval date; it never attributes all files to the compilation's acquisition date (CSO G3 ruling, 23 October 2026). Existing SourceRefs keep their own URLs and dates; the separate Drugs@FDA reference stays live unless an explicit saved snapshot date is supplied.
  • The registry holds 16 limit sentences (content/tools/drug-pipeline-explorer.ts), rendered by the LimitsBanner, the precedent view (filtered by keyword) and every export's provenance block.

Objects, Studies and hand-off

The registry declares objects: { owns: ["condition", "trial", "product"], reads: ["condition"], writes: [...], tabs: [...] } (see Storage and state for the object store).

Type Ref Learned when Facts
condition cond:<normalised words>, never mesh: by guess Its search found trials (the catalogue must have loaded) ClinicalTrials.gov: total, analysed, recruiting, data date (unfiltered searches only); openFDA: indicated product count, label data date
trial nct:<id> Opened in the inspector Title, phase, status, start, completion (with "(estimated)"), enrolment, sponsor class (never the name), modality, data date
product fda:<applications joined with +> (null beyond eight applications) Opened in the inspector Applications, type, brand, ingredients, sponsor (re-cased), original approval, marketed or discontinued, label data date; aliases are brands and ingredients

Under D-0045 the hooks refresh only records the person already holds; the preview learns nothing. Object pages get Pipeline's three tabs: a condition's Board searched when the tab opens (cards open object pages; lists and selections open the full app), a trial's record, and a product's Drugs@FDA or NDC record.

Hand-offs:

  • Out to Molecule (L3): from a US product with exactly one small-molecule ingredient, productPayload sends { name }; the inspector and product tab offer one Molecule link per small-molecule ingredient (MoleculeLinks, SendToLink). Registry crossLinks: molecule-workbench, params name, status live.
  • In from Protein (L2) and from condition objects: by condition, as above.
  • No hand-off to Protein or the bench apps.

Saved research views: WorkCaptureControls, on the section tab row (ViewNav's actions, since 836907fc), saves a PipelineWorkspaceData payload (workspace-inputs.ts, schema pipeline-research-view version 1) as a note object: the applied query state (not unsubmitted form text), the view and layout, card order, trial sort, filter, hidden columns and page, a research-context note of up to 20,000 characters, and a reference block (live or snapshot, retrieval time, API version, data timestamp, openFDA last_updated). readPipelineWorkspace validates every key against the URL schema before restoring. Reopening runs the search again and may use the existing 30-minute in-memory cache; the trials and approvals are not stored in the saved view, and the saved source date describes the saved context, not proof of unchanged results. The data may change even when the registry version metadata stays the same. Copy link carries only the supported URL query/view fields: it removes the local mine reference and excludes the research note and list-only text filter, sort, hidden columns and page. Those display details remain in the saved v1 payload. The same payload is offered as an export ("drybench-pipeline-research-view.json"). Studies and Experiments ownership follow the shared contracts (Storage and state).

Saved work and tabs. Reopening starts on Landscape, then the person can select Trials to inspect the restored list settings. The current tab is navigation rather than a saved edit: a fresh saved payload omits state.view, while an existing payload retains its prior view value for comparison. Tab changes alone neither overwrite the saved view nor create a kept draft (the CEO's 14 October instruction, commit 6a05a50c9f; clarified by docs/board/memos/2026-10-23-cto-pipeline-saved-view-contract.md). A copied public link can still name the currently visible tab.

Kept edits. Edits to a named task are kept as its draft beside the record, never written into it by the timer or by leaving (D-0068 rule 8; Storage and state). The mine open path reads the record (readSavedView), then the live task that owns it (readOpenWork) and that task's draft (readOpenWorkDraft) through keptOrSaved; a draft that readPipelineWorkspace accepts is put on screen instead of the record, and showOpened sets appliedDraft in the same update as the query, display settings and note (a refused draft is offered as unshown and stays in its row). An address alone (a bookmark, the object page) opens the owning task with the view, so the offer shows there too. reloadOpenView serves onDraftResolved: Discard puts the saved view back, its Undo re-reads the draft; both check that the open view and the account scope are unchanged before they set anything, and the search runs again only when the query differs. Two things keep an unchanged view from counting as an edit: while the opened view's search is on screen (its condition compared through cleanCondition, since a view saved before the condition catalogue loaded holds the words as typed), its reference block stands for the trials until ClinicalTrials.gov's API version or data timestamp differs (or while they are still arriving), and for approvalsLastUpdated while the approvals load, so a re-run search with a new retrieval time is not a change; and once the search, the data version and the approvals have settled, the view bumps savedOnScreen so the control takes those inputs as the task's baseline. Save is disabled until the search, the data version and the approvals have settled, so a save never captures a view whose late data then reads as an edit (REL-41 c). Pipeline has no autosave of its own.

Exports

The already-lazy workspace registers the entire export menu through the existing useExports registry: standard figures and trial/product/aggregate actions, then FDA precedent, then saved research-input JSON when available. The definition no longer imports the export factories. The same memoised trial scope and a small current-render projection supply publication and menu readiness, so an open menu follows result/list transitions without reading a previous published snapshot during render. Lazy execution still reads the latest published workspace and rechecks scope readiness. Registration is removed on unmount; actions appear after the workspace mounts.

Writer-only README and FDA precedent evidence copy lives in content/apps/drug-pipeline-explorer.export-evidence.ts, imported directly by the lazy writers. export-scope-lines.ts formats trial scope evidence with the default registry filters. The eager menu imports only the lightweight identity/capture/readiness helpers from export-scope.ts; workspace copy does not reference or re-export the writer evidence. Menu descriptions and delivered wording are unchanged.

FDA precedent: filtered result (CSV) delivers a mandatory ZIP containing the exact 29-column CSV, README and provenance, including when the settled filtered result has zero rows. PrecedentView publishes its existing buildPrecedent/filterRows output and parsed filters before table text matching, local sort, paging and selection. The app-local descriptor retains the approvals result identity, condition and raw filter identity; the lazy writer reads the current workspace after import and refuses stale/loading/failed/unmounted descriptors. Ownership-checked effect cleanup cannot clear a newer descriptor. The existing export registry updates readiness while a menu is open.

exports-precedent.ts projects existing values only: no alternative calculation or join. Indicated rows with unchecked wording remain included and use labelUse with the global unchecked application set. Duplicate compilation membership stays as separate product rows. Missing receipt on a joined row exports receipt-not-recorded; a missing computed result with joined dates exports invalid-review-interval. Original-approval fallbacks require explicit provenance and no recorded detail failure. Unknowns remain blank/null; designation qualifiers, original per-application records, conflicting approval dates, and published-letter intervals/rescissions survive. Each row cites the relevant raw compilation row and all source files with their original hashes and independent retrieval dates. G4’s actual openFDA acquisition value is shared with G3 through openfdaProvenanceSource; cached live and snapshot acquisition retain their original values, and legacy unknown acquisition remains null rather than export time.

exports-precedent-bundle.ts uses the existing tabular writer with automatic provenance columns disabled to preserve the ordered schema, while retaining shared CSV quoting and formula protection. The writer explicitly selects its existing ZIP result; no licence trick or shared delivery API changes are used. README and provenance retain condition, normalised filters, base/exported counts, population exclusions, unchecked identifiers, approval caps/unresolved records, snapshot origin, source dates and stale warning. Today's label match does not establish original indication equivalence; absent published letters or designation evidence cannot establish a negative finding. The write-once synthetic projection fixture was committed before runtime implementation and does not close Q-19's independent scientific review. The file-level FDA sources are the panel's own (precedentSnapshotRefs in precedent-provenance.ts): each carries its snapshot's date-only retrievedOn as retrievedAt (never a midnight-UTC time), the compilation's edition as version, the breakthrough lists' current data date and the CRL file's openFDA last_updated as dataAsOf, and leaves dataAsOf out when the source states none (the CSO's ruling of 24 October 2026, docs/board/memos/2026-10-24-cso-pipeline-crl-and-snapshot-dates.md). The CRL limit reads "An application with no listed letter may still have received a complete response letter."

The global menu distinguishes Trials: full loaded result (CSV) from the additive Trials: current list (CSV). Full export retains registry row order, imaging-only records and all existing columns. Current list uses the workspace's existing orderedTrials projection across every page, with the chart filter, visible-column text matching and table sort. Paging, row selection and hidden table columns do not narrow the CSV schema. An empty current list disables its action.

export-scope.ts captures ordered rows and matching-column arrays with copied, frozen controls, bound to a single settled query/result identity. The workspace publishes this descriptor in a layout effect; execution reads the latest store after the lazy writer loads and rejects loading, failed or stale query/result combinations. Snapshot states carry an explicit query association. Inspector navigation remains independent.

Both trial tables and all trial-derived aggregate ZIPs carry README scope, source origin, applied registry query, loaded/analysed/matching counts and exported table-row count. Later-page failure is disclosed independently of the page cap. Snapshot scope distinguishes kept recent rows from stored aggregate counts and states its default registry filters. Current-list README additionally describes chart/text filters, sort and visible matching columns. Figures retain their existing populations and metadata; interrupted-download README qualifications are tabular-export evidence, not a new shared figure metadata field.

exports.ts and the workspace registration list seven data exports; availability follows each source result and view; exports-run.ts (loaded on demand) builds them with the suite's buildTabularExport. ClinicalTrials.gov exports deliver a ZIP with CSV, provenance and README. US products (openFDA, CC0) deliver the CSV as the primary file under C07, with per-row source columns; essential evidence distinctions therefore live in that CSV, not only in its optional bundle README.

Export Rows Columns
Trials: full loaded result Every loaded trial, including imaging-only records (stored recent records for a snapshot, with a README note) nct_id, title, phase, status (code), why_stopped, start, start_estimated, primary_completion, primary_completion_estimated, lead_sponsor, sponsor_company, industry, enrolment, interventions, lead_modality, modalities
Trials: current list Every row in the ordered chart/text-filtered list across all pages Same complete columns as full loaded result
US products Every product in the approvals result, including patient-group-only and ruled-out labels; existing order/grouping retained brands, ingredients, sponsor, application_type, applications, first_approved, modality, discontinued, label_use, then Stage G's source_date, date_kind, details_not_loaded, followed by the suite's provenance columns. label_use retains indicated, indicated_unchecked, patient_group_only, ruled_out (REL-221). See source semantics below.
Phase by status aggregates.phaseStatus phase, five status groups, total
Sponsors Top 12 companies company, class, trials, registered_as
Modality by period aggregates.modalityByPeriod period, the ten groups, total

Product source semantics (Stage G, 23 October 2026). exports-products.ts is a pure projection loaded only with the export writer. first_approved now contains the selected date only when dateKind explicitly says original-approval and notLoaded is not true. It is the original application's approval, not necessarily approval for the searched condition. source_date preserves the exact stored normalized date; date_kind is original-approval, marketing-start, or unknown for legacy/missing provenance. The selected date and grouping are unchanged, including mixed-source groups: the exporter never selects an alternative from originals. details_not_loaded=true records a failed request. False means no failure was recorded, not proof of complete or successfully retrieved details. On recorded failure discontinued is blank; otherwise its existing stored boolean is preserved (Drugs@FDA marketing status or NDC marketing-end handling, not a safety-withdrawal finding).

This is a semantic correction to values under existing headers, with new fields appended after label_use; consumers needing the formerly exported date can use source_date with date_kind. The independently specified synthetic semantic fixture pipeline/drug-pipeline-explorer/fixtures/product-export-semantics-v1.json was committed before implementation. exports-products.test.ts exercises the actual CSV writer, unchanged grouping, unknown/failure states and escaping; product-evidence.spec.ts checks downloaded CSV rows and per-row provenance at desktop, phone and Dark. These are projection checks, not new FDA evidence or independent clinical validation.

Each provenance block carries the app version, the condition, the canonical link, the source with dates and modification statement, every limit, the regulatory, not-medical-advice and not-forecast statements, and, for the trial-based exports, a capped flag. The README notes a capped search and a snapshot; since Wave 4 the trials README also carries the coverage lines (scope, other registries, analysed of total, the field counts, the standing qualification).

Figures (Wave 4). Two items lead the Export menu's figure group, "Timeline by phase (figure)" (pipeline-figure-timeline) and "Lead sponsors (figure)" (pipeline-figure-sponsors), SVG or PNG on a white or transparent background through the suite's figure dialog. exports-figure.ts, loaded in the item's prepare, draws each into a hidden host so the SVG writer can read styles. The provenance block is the trials block (ClinicalTrials.gov source, capped flag); the caption gives the scope ("{analysed} of {total} trials analysed; {filters}."), for the sponsor figure "Lead sponsor as registered, merged by sponsor-alias table; collaborators, licensors and partners not counted. (sponsor-aliases-2026-10-05)", and "Registered trials, not an asset inventory. Counts are trials, not drugs." Individual-class sponsors are one unnamed bar ("Individual investigators ({n} trials)"). Each item is disabled before a search, and on an older snapshot without the counts. Files are named …-timeline-by-phase-… and …-lead-sponsors-…. There is no JSON chart export and no report (see below). Export behaviour is checked in e2e/drug-pipeline-explorer/exports.spec.ts; the shared writers are in Storage and state.

Methods and checks dialog

The methods and checks dialog (D-0085; Shared shell, Registry and content) reads content/apps/drug-pipeline-explorer.methods-sheet.ts: one group per view, quoting the docs page's defaults and test entries, with the statement that the modality labels' accuracy has not been measured. FDA precedent carries REVIEW_TIME_METHOD and the four draft fixture sets of drug-pipeline-explorer.verification.ts with its status line verbatim. definition.methods.group() returns the store's view.

Verification

Unit tests

Run by npm test. The Stage G startup ran exports-products.test.ts and openfda.test.ts: 30 passed. This is not a current full-suite result; validation and outstanding checks are recorded in docs/progress/pipeline-stage.md.

File Covers
ctgov.test.ts Advanced filter, URLs and paging parameters, condition cleaning, partial dates and day numbers, phase buckets, study parsing, default filters
openfda.test.ts Label searches (presets, phrases, possessives, accents, Greek letters, refusals), URLs, application counts, Drugs@FDA and NDC parsing, grouping, small-molecule inference, not-loaded rows, negation, letter matches, condition matcher, NDC tracing and kits, parent names
src/apps/.../exports-precedent.test.ts Independently authored synthetic 29-column projection fixture, all specified cells and scope cases, actual build/filter membership, source metadata/null acquisition, readiness identities, zero/populated ZIP contents and formula protection
src/apps/.../exports-products.test.ts Independent product-export semantic cases, original versus marketing-start/unknown dates, recorded failures, unchanged grouping/order, CSV escaping and formula protection
modality.test.ts Hand-written cases per group, placebo handling, diagnostics, known misreadings, conjugates, other names, trial-level combination, rule-table well-formedness
why-stopped.test.ts Rule order, non-causal safety, board concerns, inability to recruit and stated causes, denials
aggregate.test.ts Quantile, status groups, duration eligibility, counts, imaging-only exclusion, example selection
coverage.test.ts (3 tests; Wave 4) Coverage, start year × phase × date type, undated, sponsor counts and the cut at 12, the figure's sponsors and individual-class total, all exactly equal to coverage_oracle.py on every frozen search; asserts the fixture holds no imaging-only trial (so the oracle's all-trials counts and the app's counted set are the same); and fails if any non-test file under src/ mentions drug-pipeline-explorer/fixtures
filters.test.ts, sponsor-aliases.test.ts, snapshot.test.ts Draft filters and URLs; sponsor merging; snapshot staleness on fixed clocks, shipped snapshot terms and approvals, index agreement
src/apps/.../url-schema.test.ts Fragment round-trip, legacy query, invalid values, defaults, view=odds alias, precedent keys, hand-off key narrowing
landscape-model.test.ts, xfilter.test.ts Headline, stages, bridges, card orders, matrix and picks, timeline and pin clustering; every cross-filter dimension against the fixture
src/apps/.../display-dates.test.ts The CSO's display-date case table (display-date-semantics-v1.json, 12 cases plus the whole set): Board date and description, timeline pin, before-axis count and unmarked note, sublabels, precedent cell, inspector and citation, each compared as written (SCI-247)
objects.test.ts, not-found.test.ts, inspect-words.test.ts, guards.test.ts Object inputs and refs; not-found rule; inspector titles; semantic colour tokens only and house style in the app and tool folders
precedent/review-missing.test.ts Every independently authored missingness case, legacy fallback precedence, unchanged computed guards and exact short/full content
precedent/nme.test.ts Join, review time, designations, missing reasons, converted and relicensed joins, figures and filters, against products.json and the shipped snapshots
precedent/examples.test.ts Every product of the seven example snapshots against examples.json
precedent/crl.test.ts Letter joins against crl-joins.json; field stripping; no forbidden CRL key in any shipped JSON under public/data/; the live count's URL
precedent/pivotal.test.ts NCT extraction against section14.json; label choice; one label and one ClinicalTrials.gov call per product
precedent/confirm.test.ts Name normalisation and the name join's precision on names.json
precedent/snapshot-age.test.ts Precedent envelope (five files, licence ids known to content/apps/infra-texts.ts, SHA-256s, no Friends of Cancer Research file); calendar checks below

Calendar-dependent tests

precedent/snapshot-age.test.ts compares the shipped files with today's date: it fails when any example snapshot was saved more than 46 days ago (60 less 14), or when any precedent snapshot is more than 76 days old (90 less 14). npm test therefore turns red on every branch once the monthly refresh is missed. The full set of staleness checks is in Data pipeline.

Fixtures: drafts awaiting independent review

The four precedent fixtures under src/components/tools/drug-pipeline-explorer/precedent/fixtures/ were transcribed from FDA documents in the build session on 5 October 2026 by the scripts in pipeline/drug-pipeline-explorer/precedent/fixtures/. Each says in its status that it is a draft "not independently checked (Q-19)", and the tests assert that wording. Q-19 in docs/board/open-questions.md requires a reviewer with regulatory-affairs experience to sign them before the view leaves preview.

Fixture Contents Oracle
products.json A seeded (20261005), stratified sample of 48 compilation products (the brief asked for 50) with receipt dates quoted from Drugs@FDA approval letters (URL, page, SHA-256), Drugs@FDA dates and review priority, breakthrough list membership; four recorded disagreements between a letter and the compilation (Entereg, Lymphoseek, Baraclude, Trelstar Depot, per docs/progress-precedent.md) Approval letters and Drugs@FDA
crl-joins.json 20 published letters, 14 joins and 6 negative cases Application number on the PDF's first page or the file name, date from the file name, Drugs@FDA approval
section14.json 30 labels, 10 naming no trial DailyMed PDF of the same set id and version
examples.json, examples-2026-10-17.json Every product of the seven examples, one write-once file per snapshot refresh (5 October 2026, wording rule 1; 17 October 2026, rule 3) A second implementation in Python over FDA's spreadsheet
names.json 100 Project Confirm cases, 50 true and 50 false Drugs@FDA approvals on the row's date with a letter

The engine reads the compilation, not the letters: where a letter and the compilation differ, the test requires the difference to be recorded in the fixture and keeps the compilation's value. The fixtures are .json files in a fixtures/ folder, so scripts/check-write-once.mjs treats them as immutable once they are on main (see Data pipeline).

The coverage oracle and the frozen search (Wave 4)

pipeline/drug-pipeline-explorer/freeze-ctgov.mts froze one search as a write-once test fixture, built with the app's own studiesUrl and default filters and kept as received: fixtures/ctgov/cystic-fibrosis@2026-10-11/ holds request.json, version.json (API 2.0.5, data as of 2026-10-09T09:00:05), pages/001.json (751 trials, one page) and manifest.json (sha256 of each file). It refuses to overwrite a directory or to freeze a search over 2,000 trials. Nothing under public/ holds it, and app code never reads it (a test checks). coverage_oracle.py re-implements the date, phase-bucket and sponsor-alias rules from their documentation, without importing any TypeScript, and wrote cystic-fibrosis@2026-10-11.expected.json beside it (it refuses to overwrite). The comparison is exact integers. Whether frozen ClinicalTrials.gov responses may be kept as fixtures under its "no static copies" wording is held for the General Counsel.

Apart from these counts there are no numerical oracles for the trial-side analytics (aggregations, classifier, stop reasons); those rely on hand-written unit cases and the cross-filter consistency test. See Oracles and verification.

Live checks

scheduled.yml runs weekly:

  • node pipeline/drug-pipeline-explorer/precedent/verify.ts: the Drugs@FDA original-submission fields (on BLA761269), the label identity and Clinical Studies fields, the CRL count call and permitted fields (one record read in memory, nothing printed), the compilation's 27-column header and the surrogate table's four sheet headers (read from the XLSX without a library), that both breakthrough files are still PDFs, Project Confirm's four HTML table headers, and the openFDA request count of the seven examples' approvals against approvalsCeiling (12), counted by wrapping fetch.
  • python pipeline/drug-pipeline-explorer/verify_stems.py: every stem against the WHO stem book.

Browser tests

e2e/drug-pipeline-explorer/ runs offline: ClinicalTrials.gov answers from fixtures/ctgov-huntington.json and ctgov-version.json for every search; openFDA answers are routed per spec.

Spec Tests Covers
workspace.spec.ts 15 Welcome examples; not-found with suggestion; Board and Console from the address; each view from the address; Board/Console persistence and link; keyboard-only flow; Board cards and stage links; matrix selection, clearing and focus; lane count stability; cross-filter chip; reduced motion; axe, CSP and no sideways scroll
precedent.spec.ts 5 Request budgets; a product with citations, letters and label trials; filters in the link; view=odds; 375 px cards by keyboard; barred words
exports.spec.ts 5 Disabled before a search; trials ZIP; count tables; current-list order; interrupted-download qualifications (historical phone skips retained)
export-scopes.spec.ts 8 Unskipped desktop/phone/Dark coverage of trial card/table ordering and partial downloads, plus populated/stale/zero/loading/failure precedent ZIPs, leaving the view, and truthful joined-missing table/inspector text
product-evidence.spec.ts 1 Products download using the semantic fixture: date meaning, failed details, column order, label verdicts and per-row provenance; execution still outstanding at Stage G startup
display-dates.spec.ts 1 SCI-247: on the sickle cell disease snapshot, CASGEVY's NDC marketing start reads as such on the Board card and its description, in the timeline's unmarked note and in the FDA precedent inspector, never as an approval
routine-week.spec.ts 4 Stage G's routine week as a user on desktop, phone and Dark: search and the coverage line; a chart and text filter, a trial and its registry link; current-list and full-result exports with their scope lines; a capped search's README; FDA precedent's absence qualifications and CRL note on every row; Back from the Molecule hand-off to the same view and condition. Saving and reopening is e2e/objects/work-draft-pipeline.spec.ts
free-path.spec.ts 1 The free path: signed out, all seven data exports download (the five trial-derived ZIPs, US products and the cited precedent ZIP) and no file carries tier text (account, upgrade, paid, subscription)
shell-focus.spec.ts 4 Share dialog focus, help menu and app switcher focus return, reduced motion
discovery.spec.ts 3 Wave 4: the coverage line and details with axe at 375 px and desktop; tl by keyboard; both figures downloaded as SVG with provenance and caption

The Wave 4 record and the CTO's merge condition 1 report that the Pipeline failures seen on the merge candidate (exports.spec.ts:50 and :68, shell-focus.spec.ts:69, :87 and :108, workspace.spec.ts:345) also fail at the wave's base 83d1e09, built and run again on 11 October 2026; they are logged as pre-existing, not fixed. These are historical results, not current-base exemptions. Stage G has not yet executed its browser checks.

e2e/objects/pipeline-objects.spec.ts (3 tests) covers the condition, trial and product object pages, using e2e/drug-pipeline-explorer/fixtures/ctgov-study-NCT07451613.json and drugsfda-aricept.json through e2e/objects/helpers.ts. e2e/objects/send-to.spec.ts covers L2 (Protein to Pipeline) and L3 (Pipeline to Molecule).

Known gaps, defects and divergences

Defects and risks found in the code:

  • A snapshot refresh needs a new dated examples fixture. precedent/examples.test.ts compares the shipped snapshots with the examples fixture whose retrievedAt matches theirs (fixtures/examples.json for 5 October 2026, examples-2026-10-17.json for the rule-3 regeneration of 17 October). Since the fixtures are write-once, a refresh writes a new dated file with build_examples.py examples-<date>.json, which reads FDA's spreadsheet into .cache/cder-nme.xlsx (check its SHA-256 against cder-nme.json). The scheduled snapshots.yml builds it (since 17 October 2026, the CTO's memo docs/board/memos/2026-10-17-cto-snapshots-examples-step.md) and adds it to its pull request; a second refresh on the same day fails rather than replace a published fixture. Old dated fixtures are never pruned. snapshot-age.test.ts fails npm test once the example snapshots pass 46 days (from about 3 December 2026 for the current files).
  • Corrected export semantics. The label-verdict omission was already fixed by REL-221 on 17 October (label_use); the earlier paragraph here was stale. Stage G corrects the remaining date/detail-loss defect in exports-products.ts: marketing starts and unknown dates are no longer called original approvals; recorded detail failures travel in the CSV and withhold discontinuation status. See Exports and the Stage G record for checks/landing status.
  • Stale snapshot dates in user-facing copy. The limit at content/tools/drug-pipeline-explorer.ts:107 says the snapshots were saved on 28 September 2026 with ClinicalTrials.gov data of 25 September 2026; the shipped snapshots were saved on 5 October 2026 with data of 5 October. The same stale dates are in the table of pipeline/drug-pipeline-explorer/README.md. Any refresh makes a hard-coded date wrong again; the sentence is not generated from index.json.
  • Unused data loaded. views/precedent-data.ts:51-56 fetches project-confirm.json (about 109 kB) every time the view opens, although nothing displays it (PrecedentData.confirm is required by the type only). PRECEDENT_FILES.surrogate is declared and never used.
  • Script comments describe behaviour the app does not have. pipeline/drug-pipeline-explorer/precedent/fetch-surrogate.py (docstring) says the app shows surrogate rows naming the condition; it does not load them. fetch-project-confirm.mts (docstring) says the app joins Project Confirm in designations.ts and shows "not joined"; the join is in confirm.ts and is not shown.
  • Corrected joined missing-review reasons (23 October 2026). The independent 17-case synthetic fixture review-missing-semantics-v1.json was committed before repair. Joined missing receipts and invalid intervals now retain their compilation membership in the domain, CSV, table and inspector wording. Computed guards, calendar arithmetic, months and method version remain unchanged; Q-19 remains open. The earlier G3 projection oracle continues to test adverse legacy inputs without preprocessing or rewriting them.
  • Latent: the live letter count is capped at 1,000 terms (CRL_COUNT_URL, precedent/crl.ts:28). The endpoint holds 458 letters today; past 1,000 distinct application terms, newer letters would go unnoticed. The term format the parser expects ("NDA 209531") is asserted with a fixture; verify.ts checks only that terms are strings, so a change in openFDA's term format would silently disable the newer-letter notice.
  • The CTO's ruling to stop the letter check when meta.last_updated matches the snapshot (docs/progress-precedent.md) is not implemented; the code always compares counts. Harmless, but a divergence.
  • Hard-coded edition. The compilation URL (https://www.fda.gov/media/177921/download), its title "1985-2025" and LICENCE_READ_ON = "2026-10-05" (in both precedent/_common.py and _common.mts) are constants. FDA's next annual edition needs a manual update of URL, title and the verify header list; a monthly refresh never updates the licence read date, which scripts/licences.mjs requires to be within twelve months.
  • Duplicated constant. ctgovSource in src/apps/drug-pipeline-explorer/provenance.ts:37 computes the snapshot expiry with a literal 60 rather than SNAPSHOT_MAX_AGE_DAYS.
  • Timeline start differs. The Console timeline starts at 1995 (views/console.tsx:30); the chart model's TIMELINE_FROM is 2000.
  • Sponsor class per company is taken from the first trial seen (aggregate.ts:203), so a merged company with trials under different sponsor classes shows one class.
  • NDC discontinuation uses the wall clock (parseNdc default today), so a snapshot's NDC-only products are marked discontinued as of the run date.
  • Size budget. The audit of 10 October 2026 recorded the Pipeline definition chunk at 295 KB against a 25 KB budget (docs/science/2026-10-10-scientific-audit-of-the-7-to-10-october-changes.md, section 1). Not re-measured for this page.
  • Comment inconsistencies. openfda.ts:536 says sponsor names are shown as the FDA records them, while the views re-case all-capital names with tidyName; landscape-model.ts:264-265 has a duplicated doc comment.
  • Individual sponsors on screen. The exported sponsor figure groups individual-class lead sponsors into one unnamed bar, but the on-screen top-sponsors chart and the Sponsors CSV still come from aggregates.sponsors, which does not separate them, so an individual investigator can be named there. Whether that is acceptable is held for the General Counsel (CPO Wave 4 held item).
  • Corrected 11 October 2026: explicit N/A and missing phase are now distinguished in new coverage records and exports. Historic snapshots disclose when that distinction was not retained; see the correction below.
  • Figure attribution unverified. Whether the suite's figure dialog prepends the data-as-of date and the ClinicalTrials.gov attribution before the Pipeline caption was left unverified in the CPO's merge review; figureCaption itself carries neither.
  • Size budgets (Wave 4). The Wave 4 record gives pipeline-definition-chunk at 26.8 KB (over its 25 KB budget at the wave's base, 26.5 KB, and grown 0.3 KB within the wave's allowance); the new workspace, coverage and figure chunks were born green.

Divergences from the design documents (the build spec is historical; these are recorded so a reader does not assume they exist):

  • Not built from the build spec: the module Worker (flags.workers: false), the IndexedDB result cache (an in-memory 30-minute cache instead), the 28-field trial record, IndexedDB sessions and the sample session (saved research views replace them), JSON exports and SVG or PNG exports of charts other than the two Wave 4 figures, "Cite this view", the printed report, the optional openFDA key, Diagnostics, and every stretch view (readouts, conditions facet, watchlists, changes, sponsor view, file-backed sessions).
  • Not built from the precedent brief: XLSX with Provenance and Limits sheets (the cited CSV ZIP is implemented above); the "FDA precedent" object-page tab (§4A); dated indication precedent pages (§5.3); CIRS benchmark medians (CIRS is an excluded source); a modality-tag precision fixture; the share of labels naming NCT numbers, which the brief asks to measure and publish; phase B.
  • Changed from the brief by ruling: breakthrough lists join exactly by application number (the brief said they had none); CRL field names follow openFDA (approver_*, not approval_*); the letter check is a count call, not a search; KPI figures follow the filters; "pivotal" is not used; the Project Confirm join uses brand, ingredient set and date rather than ingredient and sponsor.
  • Release gates open: fixtures unsigned (Q-19); nine items held for the General Counsel (H-01 to H-09 in docs/board/open-questions.md), including the CRL personal-data position and the statement wording. Legal conclusions on these are held for the General Counsel.
  • Open items in docs/NEXT.md: drawer overlap at intermediate desktop widths, sponsor-name formatting, remaining shell focus and overlay requests; precedent fixture review, export confirmation, Project Confirm and surrogate scope, fixture-helper URL encoding and phase-B plans.

Scientific limits that are by design (registry limits and the scientific audit): trial counts are not asset counts; industry lead sponsor is not proof of ownership; modality rules are text heuristics and leave company codes unclassified; FDA application approvals are matched on label wording and the date shown is the original application's approval, which can predate the condition's approval; a discontinued listing is not a safety withdrawal; CBER products are not in the compilation; review times exist only to the end of 2025.

Extending and refreshing safely

Refreshing data. Use the monthly snapshots.yml workflow, or run the commands in Data pipeline. Review the diff: index.json dates and counts, per-preset product counts, precedent row counts, coverage dates and checksums. Rerun snapshot.ts whenever the classifier, the aggregations or the openFDA procedure change, because snapshots embed their output. After a refresh, examples.test.ts looks for the examples fixture built from the new snapshots, so build a new dated one with python pipeline/drug-pipeline-explorer/precedent/fixtures/build_examples.py examples-<YYYY-MM-DD>.json (the fixtures are write-once; the old ones stay). The scheduled workflow does this itself.

A new FDA compilation edition. Update the URL, the title, HEADER in fetch-cder-nme.py and COMPILATION_HEADER in verify.ts if columns change, the "1985-2025" text in precedent-provenance.ts and the registry source, and the limit that says "approvals to the end of 2025". The coverage dates flow into missingReason automatically.

A new example search. Add it to PRESETS with a hand-checked labelSearch, regenerate the snapshots (which also changes the search catalogue's brand aliases when scripts/build-search-catalogs.mjs is rerun), add expected values to the examples fixture, and check the openFDA request count in verify.ts stays within the published ceiling or update PRECEDENT_REQUESTS and its copy.

Classifier changes. Add a stem only if it is in Part II B of the stem book and run verify_stems.py; give it a minLength and an exclude list for English words; add positive and negative cases to modality.test.ts; regenerate the snapshots. The count in the method copy is computed (SMALL_MOLECULE_STEMS), but the "146 more" and "151" figures in content/tools/drug-pipeline-explorer.ts are typed and must be updated by hand.

Aggregates and snapshots. A new aggregate field must be optional (snapshots embed Aggregates), any view or export that reads it must say why it is unavailable on an older snapshot, and its counts should be checked against coverage_oracle.py (or a new oracle) on a frozen search: freeze a new one with freeze-ctgov.mts into a new dated directory, since fixtures are write-once. Keep the parser in ctgov-parse.ts and shared constants in import-free modules, so the workspace and figure chunks stay within their budgets (pipeline-workspace-chunk 166.5 KB, pipeline-coverage-chunk 1.2 KB, pipeline-figures-chunk 11.9 KB).

New URL keys. Add them to pipelineSchema with a validator, keep the condition the only subject, never put user-authored text in a link, and add round-trip and rejection cases to url-schema.test.ts. Removed views go into VIEW_ALIASES so old links keep working.

Showing Project Confirm or surrogate endpoints. Both need a CEO decision. Project Confirm's join must reach the CSO's 98 percent precision threshold on a larger fixture before it is shown. Once shown, provenance must cite the source table and row, and the brief's "not joined" wording applies.

FDA precedent rules. Keep the join to exact application numbers; never estimate a review time; keep "programme did not exist" apart from "No"; never read a CRL field outside PERMITTED_FIELDS (the test that scans public/data/ enforces this for shipped data); never add the words the e2e test bars. Any feature that scores, ranks or predicts approval would bring the patent questions in the brief's §10.6 into play and is held for the General Counsel.

Before finishing a change, run npm run lint, npm test, npm run build, npx tsc --noEmit and, for anything visible, npm run test:e2e (Build, test and CI). Hosts must be declared in the registry, since the Content Security Policy is generated from it (Registry and content). Collect new defects in Known issues.

Scientific audit correction — phase completeness (11 October 2026)

Parsed Trial rows now retain phaseRecording independently of the chart bucket: applicable, explicitly not applicable (NA), missing (absent/empty phases), or unknown (unrecognised codes). Coverage.phaseRecorded retains its historical applicable-phase-only meaning, so existing frozen counts are not rewritten. New aggregates add phaseNotApplicable, phaseMissing and phaseUnknown. The completeness table counts applicable phase plus explicit N/A as recorded, and only absent fields as not recorded. The detail text and export README expose all four counts. Older snapshots lacking these fields explicitly say the N/A/missing distinction is unavailable; a parsed legacy trial with only the combined bucket is unknown, never silently missing. Existing charts retain the combined Not applicable bucket with a scope note explaining that grouping. No numeric success probability or drug count is inferred.

coverage.test.ts keeps the original frozen Python oracle unchanged and adds independent synthetic regression cases covering all four source states and historical ambiguity. Date-type uncertainty is unchanged by this correction.

Public reference workflows (16 October 2026)

Two public routes start with dated, selected six-study ClinicalTrials.gov captures processed through the native parser. The native 60-day expiry and snapshot alarm apply. Subsequent user searches retain normal live behaviour. See Public reference workflows for the shared route, state, source and export contracts.