Skip to content

21 Slice Clinical Trials

Status: CONVERGED NCT CORE / ATOMIC QUERY FAMILY / ChiCTR FIXTURE PROFILE. The official NCT source spine, protocol/result grains, governed eligibility extraction, provenance, and generation-protected seventeen-model q_trials family are implemented and fixture-tested. ChiCTR remains source-scoped and fixture-backed; live scraper hardening is an explicit source expansion. The optional eligibility task is provider-activation gated. Denoising state: converged (noise -> rough -> sharp -> converged) Updated: 2026-07-25

Model the clinical-trials pipeline end-to-end at the same standard as every other domain slice. It must define its own source snapshots, normalized records, evidence claims, entity resolution, LLM-assisted resolution/verification, curation overrides, canonical assets, SQL read models, checks, operations, and tests without becoming the template for other slices.

Clinical trials are the hardest useful first slice because legacy Data Gov already treated them as a weekly, multi-source, LLM-heavy pipeline: ClinicalTrialsWorkflow collected/reconciled trials, extracted study plans/outcomes/conditions/eligibility, and then triggered ClinicalTrialEligibilitiesWorkflow for disease/biomarker resolution (../bioloupe-data-gov/docs/clinical-trials.md, ../bioloupe-data-gov/app/workflows/clinical_trials_workflow.rb, ../bioloupe-data-gov/app/workflows/clinical_trial_eligibilities_workflow.rb).

Clinical trials is one equal-priority domain slice. It must exercise its domain model, entity resolution, LLM verification, curation overrides, asset graph, contracts, operations, tests, and q_* query layer, while resolving shared identities through owner spines such as 29-slice-drug-identity.md.

flowchart LR
  CTGov[ClinicalTrials.gov API]
  ChiCTR[ChiCTR]
  Raw[Trial source snapshots]
  Normalized[Normalized trial records]
  Evidence[Trial design / intervention / endpoint / eligibility claims]
  Resolution[Drug, disease, biomarker, organisation resolution]
  Verify[Verifier / accepted basis]
  Curation[Reviewed curation overlay]
  Canonical[Canonical trial assets]
  Query["q_trials family"]

  CTGov --> Raw
  ChiCTR --> Raw
  Raw --> Normalized --> Evidence --> Resolution --> Verify --> Canonical --> Query
  Curation -. optional override .-> Canonical
SourceGreenfield roleSource identifierLegacy evidenceWeb/reference evidence
ClinicalTrials.gov API v2authoritative NCT registry source for study protocol, status, contacts, interventions, arms, outcomes, eligibility, references, result groups, outcome analyses, baseline characteristics, participant flow, and adverse-event resultsNCT{8 digits}Data Gov used CTGov for version/change reconciliation and AACT as a convenience mirror (../bioloupe-data-gov/docs/clinical-trials.md, clinical_trials:versions:update)ClinicalTrials.gov publishes a modern REST API, OpenAPI spec, /version, paginated /studies, /studies/{nctId}, and searchable field metadata: https://clinicaltrials.gov/data-api/api and https://clinicaltrials.gov/api/oas/v2
AACToptional reconciliation/audit mirror for NCT data; not a greenfield source spine and not required for canonical/query factsnct_id plus mirror-local table ids/group codeslegacy connects read-only to AACT for discovery/results because AACT exposed convenient SQL tables (../bioloupe-data-gov/docs/clinical-trials.md, lib/tasks/clinical_trials/outcomes.thor, ../bioloupe-ai-solutions/data-collection/src/clinical_trials/collect_results_full.js)AACT is a PostgreSQL mirror of public ClinicalTrials.gov protocol/results data, loaded from ClinicalTrials.gov and roughly one day behind; it does not include study change history: https://aact.ctti-clinicaltrials.org/points_to_consider
ChiCTRadditional registry source for Chinese trials not covered by ClinicalTrials.gov; operationally high-risk due no API / anti-botChiCTR{number}legacy scrapes weekly with Ferrum/headless Chrome and imports XML/markdown (../bioloupe-data-gov/docs/chictr.md, lib/tasks/clinical_trials/chictr_weekly.thor)ChiCTR public search/registration site: https://www.chictr.org.cn/
PubMed referencestrial-publication linkage seed; detailed publication evidence belongs in 23-slice-publications.mdPMID / DOI / registry ids in abstractsclinical_trials:references:update_for_new_trials and publication_clinical_trialsPubMed/NCBI E-utilities and MeSH are the publication/search vocabulary family: https://www.ncbi.nlm.nih.gov/books/NBK25501/
NCI Thesaurus / EVScontrolled-vocabulary anchor for oncology diseases, biomarkers, and non-drug conceptsNCIt codelegacy disease/condition/biomarker matching calls NCI services and stores nci_code (../bioloupe-data-gov/app/models/clinical_trial_condition.rb, docs/data-model.md)NCI EVS provides cancer vocabulary resources including NCI Thesaurus and NCI Metathesaurus: https://www.cancer.gov/about-nci/organization/cbiit/vocabulary
CDISC SDTMreference vocabulary/grain guide, not a target schema cloneSDTM domains/controlled terminologylegacy has no CDISC-native model; it independently models arms, endpoints, outcomes, and eligibilityCDISC SDTM defines the general conceptual model for representing clinical study tabulation data; use as alignment reference only: https://www.cdisc.org/standards/foundational/sdtm and https://www.cdisc.org/standards/foundational/sdtm/sdtm-v2-0

Use a ClinicalTrials.gov API-first NCT spine:

  • ClinicalTrials.gov API v2 is the immutable official source-artifact spine for NCT discovery, study JSON, protocol modules, eligibility text, result modules, adverse-event sections, references, API version, source data timestamp, and exact per-study snapshots.
  • AACT is not a first-class Dagster source for canonical/query facts. It is retained only as an optional reconciliation/audit mirror because it mirrors ClinicalTrials.gov current public data and exists mainly to make SQL analytics easier. AACT ids never become greenfield identity, and greenfield q_* facts must not depend on AACT row ids.
  • ChiCTR is a first-class separate registry source family. ChiCTR can lag operationally, but the canonical model must not assume every trial has an NCT id.
  • Historical change backfill is a separate source decision. The CTGov API/OpenAPI surface used here exposes the most recent public study record; our source ledger preserves forward history from first capture. If pre-existing historical versions matter, model the ClinicalTrials.gov archive as its own source family. Do not pretend AACT solves that: AACT also excludes study change history.

Legacy correction: the original bioloupe-ai-solutions production query filtered AACT conditions, browse_conditions, interventions, and studies for oncology/hematology therapeutic trials (../bioloupe-ai-solutions/data-collection/helpers/queries_clinical_trials.js, ../bioloupe-ai-solutions/data-collection/src/clinical_trials/collect_trials.js). That proves the product intent: cohort discovery with source-visible reasons. It does not prove AACT should remain a greenfield source spine now that ClinicalTrials.gov API v2 exposes current protocol/results JSON and searchable condition/intervention areas directly.

The old system is split across two repos. Data Gov orchestrates and enriches the workflow; Bioloupe AI performs the AACT bulk pulls. Greenfield Dagster should preserve the source semantics and product intent, not port the Rails/Thor/Sidekiq/shell choreography.

Legacy capabilityWhere the old system does itWhat the old step really meansGreenfield Dagster model
Weekly clinical-trial orchestration../bioloupe-data-gov/app/workflows/clinical_trials_workflow.rbOrder AACT, ChiCTR, CTGov history, result, study-plan, condition, eligibility, reference, and notification steps.Asset dependencies, partitions, checks, and sensors. No workflow class or shell-step clone.
AACT cohort discovery and trial bulk loadData Gov calls build-clinical-trials.sh; implementation is ../bioloupe-ai-solutions/data-collection/src/clinical_trials/collect_trials.js and helpers/queries_clinical_trials.jsSelect interventional oncology/hematology therapeutic trials using study type, intervention type, raw condition text, browse/MeSH condition terms, curated disease terms, cancer keywords, and therapeutic-area terms.sources.ctgov.trial_discovery_runs and sources.ctgov.trial_discovery_candidates; CTGov query params, returned NCT ids, source search areas, matched rules, accepted/rejected scope decisions, and page tokens are replayable evidence.
AACT eligibility text load../bioloupe-ai-solutions/data-collection/src/clinical_trials/collect_eligibilities.jsPull source eligibility text and split inclusion/exclusion sections.normalized.trial_eligibility_texts from CTGov protocolSection.eligibilityModule.eligibilityCriteria; later evidence.trial_eligibility_claims extracts structured facts.
ClinicalTrials.gov version / change detection../bioloupe-data-gov/lib/tasks/clinical_trials/reconciliation.thor, eligibility_changes.thor, CtGovServiceCompare current registry/source modules, flag meaningful changes, and decide which downstream families need refresh.sources.ctgov.trial_snapshots stores full payload bytes/hash plus API version/data timestamp; ops.ctgov_snapshot_diffs hashes source modules and targets rematerialization without mutable workflow flags.
AACT stable metadata selective sync../bioloupe-ai-solutions/data-collection/src/clinical_trials/collect_stable_tables.js using change_flags_config.jsRefresh sponsors, locations, officials, study designs, and documents only when the corresponding CTGov module changed.Parse CTGov modules into stable normalized families; module-hash diffs decide whether sponsor/location/design/reference downstream assets need refresh.
AACT results sync../bioloupe-ai-solutions/data-collection/src/clinical_trials/collect_results_full.js and SELECTIVE_SYNC_DOCUMENTATION.mdWhen any result module changes, replace the full per-trial result hierarchy because AACT result row ids are unstable.Parse CTGov resultsSection as a full result-family snapshot per trial/source hash. Result-group ids such as OG000 / EG000 are source locators inside the CTGov snapshot, not canonical identity.
ChiCTR collection../bioloupe-data-gov/lib/tasks/clinical_trials/chictr_weekly.thor, app/services/chictr/*Scrape/import ChiCTR XML/markdown with run metadata and anti-bot/parser health risk.sources.chictr.trial_xml_artifacts, sources.chictr.trial_snapshots, and parser-health checks; ChiCTR remains source-scoped identity.
Study-plan extraction and component matching../bioloupe-data-gov/app/services/study_plan/*LLM extracts arms/components; matching resolves interventions to drugs/non-drug concepts with exact/manual/context/fuzzy bases.evidence.trial_design_claims and resolution.trial_entities for trial_intervention_link and trial_result_group_arm_link; curation handles disputed high-impact links.
Eligibility enrichment and disease/biomarker matching../bioloupe-data-gov/app/workflows/clinical_trial_eligibilities_workflow.rb, ParticipationCriteriaService, TrialEligibilityCriterionLLM structures criteria, then disease/biomarker candidates are searched, judged, post-processed, and QA’d.evidence.trial_eligibility_claims with provider-neutral LLM task, schema validation, quarantine, verifier receipts, and resolution through disease/biomarker spines.
ORR / derived result projections../bioloupe-data-gov/lib/tasks/clinical_trials/derived_measurements.thorCompute explicit ORR or guarded CR+PR projections without overwriting source results.Publish source-reported ORR. Do not infer ORR by summing response categories unless a future, separately versioned derivation contract proves compatible denominator, arm, population, and timepoint grain.
API controllers and analytics views../bioloupe-data-gov/app/controllers/api/clinical_trials_controller.rb, analytics bridge/dim/fact tablesProduct questions over trials, diseases, drugs, organisations, eligibility, and results.SQL-only q_trials family plus q_provenance; no REST/GraphQL consumer surface.

Implementation alignment checkpoint:

  • Current source assets now model the NCT spine from ClinicalTrials.gov API v2 shape: fixture mode uses protocolSection, resultsSection, derivedSection, and hasResults; live mode is opt-in through ctgov_source Dagster resource run config and captures /version, paginated /studies discovery, and /studies/{nctId} snapshots.
  • sources.aact.result_snapshots is removed from the required Dagster graph. AACT remains only an optional future reconciliation mirror and must not feed canonical/query facts.
  • Discovery now materializes accepted and rejected candidates in sources.ctgov.trial_discovery_candidates; q_trial_discovery_candidates publishes both accepted and rejected NCT ids with source-search provenance.
  • CTGov protocol/result modules now feed canonical and query models for sites, references, outcome analyses, participant flow, baseline characteristics, adverse events, and adverse-event totals directly from the immutable source snapshot.
  • Eligibility extraction now combines a conservative deterministic parser with an opt-in, bounded trial_eligibility_extract gap-fill. Exact artifact/quote checks, requested-facet scope, numeric-bound validation, rejection quarantine, immutable inference decisions, and projection receipts are implemented; this is not presented as an independent semantic verifier.
  • The correct source authority is ClinicalTrials.gov API v2 and ChiCTR raw artifacts. Legacy Data Gov rows and AACT mirror rows are reconciliation sidecars and product-intent evidence, not source truth.

Implemented capabilities and explicit boundaries

Section titled “Implemented capabilities and explicit boundaries”

This table distinguishes executable behavior from deliberate non-behavior. A future extension must earn a producer, contract, tests, and provenance path; this spec does not reserve placeholder assets.

CapabilityStatusNote
CTGov NCT source spine (trial_discovery_runs/_candidates/trial_snapshots, ops.ctgov_snapshot_diffs)implementedfixture default; live opt-in through ctgov_source run config.
ChiCTR source family (sources.chictr.trial_snapshots)implemented (fixture)Independent of CTGov. Live scraper/runtime hardening remains a source-breadth expansion, not hidden NCT fallback behavior.
Normalized/evidence/canonical/q_trials family (17 models incl. support grains)implementedProtocol, eligibility, status history, sites, sponsors, references, results, safety, flow, and baseline each retain their own grain. q_trial_result_groups is diagnostic/support (ADR-0005).
Eligibility extractionimplementedDeterministic claims run first. Optional trial_eligibility_gap_fill reads only needs_fresh_llm rows, defaults disabled, validates exact citations/request scope/numeric bounds, quarantines rejected candidates, and projects accepted current-source evidence with immutable receipts (docs/06,11).
Trial entity resolutionimplemented fail-closedExact/cross-reference bases may publish. Fuzzy candidates park because the live asset deliberately has no semantic verifier profile yet; adding one requires an evaluated verifier contract, not a default model call.
q_trial_references / canonical.trial_referencesimplementedExposes the registry-stated PMID/citation. Publication identity and accepted publication-to-trial links remain owned by q_publications / q_publication_trial_links (docs/23).
Trial discovery scopeimplemented at source grainsources.ctgov.trial_discovery_candidates is the single owner of accepted/rejected candidate decisions and their query/rule provenance. There is no duplicate evidence/canonical scope assertion.
Derived ORRintentionally excludedq_trial_outcomes publishes registry-reported results only. CR+PR is not assumed to be ORR across possibly incompatible groups, populations, denominators, or timepoints.
Cross-registry mergingintentionally unmergedNCT and ChiCTR remain source-scoped identities. Any future merge requires explicit cross-reference or reviewed high-impact curation evidence.
Legacy trial salvage (sources.legacy_data_gov.trial_llm_outputs -> evidence.legacy_trial_claims -> provenance.legacy_llm_reuse_decisions)implementedsource-matched legacy evidence salvage (ADR-0006); the reuse-decision ledger is a hard precondition before fresh trial_eligibility_extract provider calls, keyed by current normalized trial + LLM task, and can skip the provider only when the docs/11 minimum source/freshness/schema/facet-coverage contract passes (ADR-0005).

reported_events and reported_event_totals are live clinical-trial result facts, not endpoint-spine vocabulary. Legacy Data Gov sourced them through the AACT mirror: build-clinical-trials-results-selective.sh runs collect_results_full, which fetches reported_events and reported_event_totals from AACT, maps nct_id to clinical_trials.id, maps AACT result_group_id to Data Gov result_groups.id, inserts the two reported-event tables in dependency order, and resets the adverse_events result-change flag only after successful insert (../bioloupe-ai-solutions/data-collection/build-clinical-trials-results-selective.sh, src/clinical_trials/collect_reported_events.js, src/clinical_trials/collect_results_full.js, src/clinical_trials/change_flags_config.js). The older full script had an explicit “Ingesting reported events” step (../bioloupe-ai-solutions/data-collection/build-clinical-trials-results.sh).

Live read-only Data Gov sampling (2026-06) confirms this is not dead data: catalog estimates show reported_events around 5.9M rows and reported_event_totals around 126k rows, while _stage twins are empty outside an active sync. Data Gov light DB dumps exclude reported_events, versions, and embeddables, so a zero-row light snapshot is not liveness evidence (../bioloupe-data-gov/lib/tasks/util/db_sync.thor).

Greenfield model:

  • CTGov resultsSection.adverseEventsModule.seriousEvents and otherEvents become trial-owned safety-observation fact families, not adverse_event_terms identity tables.
  • CTGov resultsSection.adverseEventsModule.eventGroups totals (deathsNumAffected, seriousNumAffected, otherNumAffected, and matching at-risk fields) become group/event-type denominator/total families, not duplicate event rows.
  • AACT ids are never source authority. If AACT is used for reconciliation, its ids remain mirror-local locators inside a reconciliation sample; Data Gov’s selective-sync docs record that AACT result ids are unstable and results tables had to be replaced per trial.
  • Adverse-event terms resolve through 32-slice-endpoint-adverse-event-identity.md; the raw source term, organ system, assessment/vocabulary, event type, and counts stay on the trial safety fact.

Cohort filtering / trial discovery contract

Section titled “Cohort filtering / trial discovery contract”

We keep the filtering. Without it, the system either ingests too much irrelevant ClinicalTrials.gov material or loses the old product’s oncology/hematology focus. The difference in greenfield is that filtering becomes a first-class, replayable asset with provenance rather than hidden script logic.

flowchart LR
  CTGovSearch[ClinicalTrials.gov search API]
  CTGovStudy[ClinicalTrials.gov study API]
  DiseaseLexicon[Canonical disease + seed cancer keywords]
  SearchAreas[Condition / intervention search areas]
  CandidateFilter[trial_discovery_candidates]
  ApiSnapshots[CTGov study snapshots]
  Evidence[trial_scope_evidence]
  Canonical[canonical.trials]

  DiseaseLexicon --> CandidateFilter
  SearchAreas --> CandidateFilter
  CTGovSearch --> CandidateFilter --> CTGovStudy --> ApiSnapshots --> Evidence --> Canonical

Filtering inputs:

InputSourceUse
Disease keyword patternscurated disease names/synonyms + seed oncology/hematology keyword listCatch free-text Condition, title, and eligibility/title search matches.
Short-name regexescurated abbreviations such as AML, NSCLC, DLBCL where safeAvoid broad %aml%-style false positives.
MeSH / ancestor condition termsCTGov search areas ConditionMeshTerm and ConditionAncestorTerm, plus maintained therapeutic-area mappingsRecover oncology/hematology trials whose raw condition text is weak.
Intervention type filterCTGov InterventionType search/filter field and study JSON intervention moduleKeep interventional therapeutic trials; default include DRUG, BIOLOGICAL, GENETIC, COMBINATION_PRODUCT, and source-reviewed RADIATION.
Study type filterCTGov StudyType search/filter field and study JSON design/status modulesDefault INTERVENTIONAL; observational studies only enter through explicit source/reference decisions.

Read-only legacy warehouse aggregates sampled 2026-07-23 show why this gate must use the official source enum instead of legacy strings: clinical_trials.study_type contains 93,594 INTERVENTIONAL, 8,555 Interventional, and 25,137 null rows; legacy trial_arm_interventions.intervention_type is dominated by drug and biological but also uses local labels such as combination and supportive. This is liveness/data-shape evidence, not source authority. The greenfield source adapter evaluates CTGov’s uppercase StudyType and InterventionType values; later normalization handles compatibility vocabulary.

Filtering outputs:

Asset / conceptGrainMeaning
sources.ctgov.trial_discovery_runsone CTGov search capture + query/filter/page boundaryThe replayable source query contract: terms, filters, API version, data timestamp, scope-rule version, complete/bounded boundary, and actual page traversal.
sources.ctgov.trial_discovery_candidatesone candidate or rejected NCT id + filter run + matched ruleCandidate selected by disease/search-area/intervention/study-type filtering, or rejected with reason.
query.q_trial_discovery_candidatesone candidate + reasonAgent-facing diagnostics for why a trial entered or failed filtering.

Filtering rules:

  • Filtering is a candidate-selection gate, not canonical disease truth. A trial selected by keyword still needs disease resolution and verifier/contract acceptance before downstream disease facts publish.
  • Filter reasons must have provenance: CTGov API URL, query params, page token/rank when present, matched term/pattern/search area, intervention type, study type, source data timestamp, API version, and run id.
  • False-positive and false-negative corrections change the versioned source-scope rule and replay discovery. Prior candidate decisions remain immutable evidence of what the old rule selected.
  • External NCT references from labels, publications, regulators, or reviewed curation do not silently mutate this source capture. A future exact-id discovery input must emit the same source-candidate grain and provenance contract before it can fetch a snapshot.
  • Exact CTGov snapshots, not discovery hits, are the source authority for published trial facts. Discovery candidates explain why we fetched or rejected a trial; sources.ctgov.trial_snapshots carries the immutable payload used downstream.
flowchart TD
  CTAPI[[ClinicalTrials.gov API]]
  ChiCTR[[ChiCTR scrape/import]]

  RawCTDiscovery[sources.ctgov.trial_discovery_runs / candidates]
  RawCT[sources.ctgov.trial_snapshots]
  CTDiffs[ops.ctgov_snapshot_diffs]
  RawChiCTR[sources.chictr.trial_snapshots]

  NormTrials[normalized.trial_records]
  NormResults[normalized.trial_result_records]
  NormSafety[normalized.trial_safety_event_records]
  EvidenceDesign[evidence.trial_design_claims]
  EvidenceResults[evidence.trial_result_claims]
  ClaimTables[evidence.trial_claim_tables]
  DeterministicEligibility[evidence.trial_claims]
  ReuseGate[provenance.legacy_llm_reuse_decisions]
  GapFill[evidence.trial_eligibility_gap_fill]
  EvidenceEligibility[(evidence.trial_eligibility_claims)]
  LegacyEligibility[[source-matched legacy eligibility]]
  Resolution[resolution.trial_entities]
  Curation[[curation.effective_overrides]]
  CanonicalTrials[canonical.trials]
  Query["q_trials family"]

  CTAPI --> RawCTDiscovery --> RawCT
  RawCT --> CTDiffs
  RawCT --> NormTrials
  RawCT --> NormResults
  RawCT --> NormSafety
  ChiCTR --> RawChiCTR --> NormTrials
  NormTrials --> EvidenceDesign --> Resolution
  NormResults --> EvidenceResults --> Resolution
  NormSafety --> EvidenceResults
  ClaimTables --> DeterministicEligibility
  ClaimTables --> GapFill
  NormTrials --> DeterministicEligibility --> EvidenceEligibility
  DeterministicEligibility --> ReuseGate
  LegacyEligibility --> ReuseGate
  ReuseGate -->|needs_fresh_llm only| GapFill --> EvidenceEligibility
  EvidenceEligibility --> Resolution
  Resolution --> CanonicalTrials --> Query
  Curation -. reviewed overrides .-> CanonicalTrials

The discovery run and candidate tables are distinct assets because they have distinct grains and downstream uses, but one HTTP traversal computes both. They therefore share one Dagster multi_asset computation and one database transaction. This is the intended Dagster abstraction for multiple assets computed by the same operation and upstream inputs: https://docs.dagster.io/api/dagster/assets#dagster.multi_asset.

LayerAsset keyGrainNotes
sourcesources.ctgov.trial_discovery_runsone CTGov search capture + query/filter/page boundaryPersist source mode, complete/bounded scope, query params, API version, data timestamp, scope-rule version, page count, candidate count, and timing.
sourcesources.ctgov.trial_discovery_candidatesone candidate/rejected NCT id + filter run + matched ruleReplaces hidden AACT SQL candidate filtering with replayable CTGov search provenance. Dagster materializes this table and its owning run together with one multi_asset computation.
sourcesources.ctgov.trial_snapshotsone raw CTGov v2 study JSON per nct_id + pull/source data timestamp + content hashPreserve complete source payload from /studies/{nctId} including protocol and results modules, source URL, API version, and source data timestamp.
opsops.ctgov_snapshot_diffsone trial + module + prior/new snapshot pairCompare module content hashes for protocol, eligibility, conditions, interventions, outcome measures/results, adverse events, baseline, participant flow, sponsors, locations, references. Drives targeted rematerialization.
sourcesources.chictr.trial_snapshotsone content-addressed raw ChiCTR record/XML/HTML-derived snapshotPreserve exact bytes, scrape metadata, and parser version. Repeated source version labels cannot collide; byte-identical transition rows are de-duplicated during normalization.
normalizednormalized.trial_recordsone source-faithful trial per source trial idDo not resolve drugs/diseases here; normalize source fields only.
normalizednormalized.trial_result_recordsone source result object per CTGov outcome measure / group / statistic / analysisCTGov result group ids such as OG000 remain source locators, not design-arm identity.
normalizednormalized.trial_safety_event_recordsone CTGov adverse-event statistic per event group / serious-or-other section / AE termParse CTGov seriousEvents and otherEvents without collapsing them into endpoint rows.
normalizednormalized.trial_safety_event_total_recordsone CTGov adverse-event section total per event group / total typeParse deaths/serious/other affected+at-risk totals from CTGov eventGroups.
evidenceevidence.trial_claim_tablesone DDL/store ownerOwns the shared trial evidence relations used by deterministic, legacy-salvage, and inference writers. Writers depend on it instead of recreating shared tables.
evidenceevidence.trial_claimsone deterministic trial evidence rebuildWrites conservative design, condition, eligibility, result, and safety claims from current normalized source records.
provenanceprovenance.legacy_llm_reuse_decisionsone current trial + inference task decisionHard provider-call gate: not_required, reused, needs_fresh_llm, or rejected.
evidenceevidence.trial_eligibility_gap_fillone explicitly selected bounded cohort runDisabled by default; only needs_fresh_llm candidates can call the provider, and accepted outputs receive decision/projection receipts.
evidenceevidence.trial_result_claimsone claim per endpoint/outcome/arm resultPreserve statistics, units, time frames, group labels, CTGov JSON paths, and source group ids.
evidenceevidence.trial_eligibility_claimsone claim per disease/biomarker/stage/treatment-line eligibility assertionExtract from free-text eligibility; later resolved/verified, with curation overlay only if disputed.
resolutionresolution.trial_entitiesone candidate set per extracted term/subjectCovers interventions, diseases, biomarkers, organisations, endpoints. It depends explicitly on the drug, disease/biomarker, endpoint/AE, and organisation owner-name projections; DDL existence alone is not a population dependency.
canonicalcanonical.trialsone canonical trial per source registry id, plus cross-registry link if provenNCT and ChiCTR ids are source-scoped external ids.
queryquery.q_trials*agent-facing grains belowMaterialized by SQL assets after checks pass.

These are source-layer tables: no resolved identity, no LLM output, no curation state.

TableGrainRequired columns
sources_ctgov.trial_discovery_runsone CTGov search/discovery capturediscovery_run_key, source_release, api_version, source_data_timestamp, source_mode, capture_scope, is_complete_source_capture, max_live_records, scope_rule_version, query_params jsonb, page_size, page_count, source_url, started_at, completed_at, candidate_count, ingested_at
sources_ctgov.trial_discovery_candidatesone NCT candidate/rejection reason from one discovery rundiscovery_candidate_key, discovery_run_key, nct_id, candidate_state, scope_basis, matched_area, matched_piece, matched_term, study_type, intervention_types text[], has_results, rank, page_token, source_locator jsonb, snapshot_id, rejection_reason, ingested_at
sources_ctgov.trial_snapshotsone full CTGov v2 study JSON capturesnapshot_id, nct_id, source_version, api_version, source_data_timestamp, pulled_at, source_url, payload jsonb, immutable_uri, content_hash, retrieval_metadata jsonb, source_release, ingested_at
ops.ctgov_snapshot_diffsone module hash comparison for one snapshotsnapshot_id, nct_id, module_name, module_hash, previous_snapshot_id, previous_module_hash, changed, computed_at

started_at and completed_at describe a successful persisted capture. Failure status and error details stay in the Dagster run/event log; duplicating a second mutable run-state machine in this source table would create conflicting ownership. The exact discovery body hash and immutable URI live in provenance.source_artifacts.

Candidate states are exactly accepted and rejected. Fetch failures belong to the Dagster run event log, while changed scope decisions appear in a new immutable discovery run rather than a synthetic supersession state. Current deterministic scope_basis values are condition_keyword_match, condition_mesh_term, condition_ancestor_term, title_keyword_match, rejected_non_onco_heme, rejected_non_interventional, and rejected_non_therapeutic. Exact external references and reviewed curation enter through their owned evidence/curation paths rather than pretending to be CTGov search results.

module_name is a stable local label whose implementation maps to one exact CTGov JSON module:

identification -> protocolSection.identificationModule
status -> protocolSection.statusModule
sponsors -> protocolSection.sponsorCollaboratorsModule
conditions -> protocolSection.conditionsModule
design -> protocolSection.designModule
arms_interventions -> protocolSection.armsInterventionsModule
planned_outcomes -> protocolSection.outcomesModule
eligibility -> protocolSection.eligibilityModule
locations -> protocolSection.contactsLocationsModule
references -> protocolSection.referencesModule
condition_browse -> derivedSection.conditionBrowseModule
participant_flow -> resultsSection.participantFlowModule
baseline -> resultsSection.baselineCharacteristicsModule
outcome_measures -> resultsSection.outcomeMeasuresModule
adverse_events -> resultsSection.adverseEventsModule
CTGov v2 pathGreenfield targetNotes
protocolSection.identificationModule.nctIdtrial_key, primary_registry_id, nct_idtrial_key = trial:ctgov:<NCTID>; never collapse with ChiCTR without accepted cross-registry link.
protocolSection.identificationModule.briefTitle / officialTitlenormalized.trial_records.brief_title / official_titleSource text, not evidence extraction output.
protocolSection.statusModuletrial status/date fieldsPreserve source date type/precision where available.
protocolSection.designModulestudy type, phase/design fieldsDrives cohort filter and query fields; no clinical interpretation here.
protocolSection.sponsorCollaboratorsModulesponsor/collaborator source org termsResolve later through organisation spine.
protocolSection.conditionsModule.conditions plus derivedSection.conditionBrowseModule when presentcondition terms and browse/ancestor termsCandidate-selection evidence only until disease resolution accepts a link.
protocolSection.armsInterventionsModule.armGroupsdesign arm source recordsPlanned design arms; not result groups.
protocolSection.armsInterventionsModule.interventionsintervention component source recordsResolve later through drug/target/non-drug concept spines.
protocolSection.outcomesModuleplanned endpoint/outcome claimsPlanned endpoints; distinct from reported results in resultsSection.
protocolSection.eligibilityModule.eligibilityCriteriaeligibility text / eligibility claimsFeed LLM extraction + verifier/quarantine before canonical promotion.
resultsSection.outcomeMeasuresModule.outcomeMeasures[]normalized.trial_result_records / q_trial_outcomesOne row per measurement statistic; analysis rows stay separate.
resultsSection.outcomeMeasuresModule.outcomeMeasures[].groups[]source result groupsGroup ids/labels are source locators; arm link requires accepted basis.
resultsSection.outcomeMeasuresModule.outcomeMeasures[].analyses[]analysis result rowsPreserve p-value, CI, HR/OR/RR-style statistics as field-provenanced facts.
resultsSection.adverseEventsModule.eventGroups[]safety-event total rowsDeath/serious/other affected+at-risk totals are totals, not AE-term rows.
resultsSection.adverseEventsModule.seriousEvents[] / otherEvents[]safety-event observation rowsOne row per event term + group stat + event type.
resultsSection.baselineCharacteristicsModulenormalized/canonical baseline measurements -> q_trial_baseline_measurementsPreserve source group, category, statistic, dispersion, denominator, and provenance.
resultsSection.participantFlowModulenormalized/canonical participant flow -> q_trial_participant_flowPreserve period, group, milestone/withdrawal reason, count, and provenance.
protocolSection.referencesModuletrial-reference source recordsPublication slice owns detailed publication evidence.

Source locators must be stable enough to debug a fact without AACT. Prefer JSON-path-ish locators:

{"source": "ctgov", "nct_id": "NCT02481830", "path": "$.resultsSection.adverseEventsModule.seriousEvents[0].stats[1]"}

This slice introduces a clinical-trial model with separate protocol design, eligibility, and reported results subgraphs. Do not collapse them into one wide table: legacy had separate clinical_trials, study_plan_arms, study_plan_components, participation_criteria, result_groups, result_outcomes, outcome_measurements, outcome_analyses, trial_endpoints, trial_outcome_measures, and trial_arm_outcomes because those are different grains (../bioloupe-data-gov/docs/data-model.md, ../bioloupe-data-gov/app/models/).

EntityGrainCanonical key recommendationLegacy pressureNotes
Trialone registry studytrial:{source}:{registry_id}; e.g. trial:ctgov:NCT04380636, trial:chictr:ChiCTR...clinical_trials.collection_source, NCT/ChiCTR idsTrial identity is source-scoped. Cross-registry duplicates require exact cross-reference, verifier acceptance, or reviewed curation override.
Trial version / snapshotone source payload at a version/time and content hashtrial_snapshot:{source}:{registry_id}:{source_version_or_pulled_at}:{content_hash}legacy versions JSONB and reconciliation flowSource dates are not assumed unique; byte-different same-date captures append instead of colliding.
Design armone protocol/planned armdesign_arm:{trial_key}:{source_arm_id_or_hash}study_plan_armsPlanned arms differ from result groups; preserve source labels.
Intervention componentone named component within a design armintervention_component:{design_arm_key}:{component_hash}study_plan_componentsA component may resolve to a Drug, NCIt concept, procedure, radiation, device, placebo, or unknown.
Endpoint / outcome measureone planned or reported measureendpoint_claim:{trial_key}:{source_measure_id_or_hash}outcomes, trial_endpoints, endpoint_groupsKeep source title/time frame; optionally link to canonical endpoint catalog.
Result groupone reported-results analysis groupresult_group:{trial_key}:{source_group_code}legacy result_groups; CTGov OG000 / EG000-style group idsNot automatically identical to a design arm. Requires source-exact link, verifier acceptance, or reviewed curation override.
Outcome resultone measurement/statistic for an endpoint and grouptrial_outcome:{trial_key}:{endpoint_key}:{group_key}:{stat_hash}outcome_measurements, outcome_analyses, trial_arm_outcomesPreserve param type, value, CI, p-value, unit, dispersion.
Eligibility assertionone structured eligibility fact for a trial/disease contexteligibility:{trial_key}:{disease_or_context}:{modifier}:{value_hash}participation_criteria, trial_eligibility_criteriaIncludes disease, subtype, stage, prior lines, biomarker, ECOG, age.
Sponsor / collaborator roleone organisation role on a trial over a source recordtrial_org_role:{trial_key}:{role}:{org_term_hash}clinical_trials:sponsors:link_to_organisationsLink to canonical organisation only after resolution.
Trial referenceone source publication/reference linked to trialtrial_reference:{trial_key}:{pmid_or_doi_or_hash}clinical_trial_references, publications workflowPublications slice owns detailed evidence extraction.
  • A canonical trial must have at least one source-scoped registry id and a provenance receipt to an immutable raw source snapshot.
  • A q_trials row must not claim a resolved drug/disease/biomarker/organisation unless the corresponding resolution subject has an accepted deterministic/LLM verifier basis, exact allowed basis, or reviewed curation override.
  • Protocol design arms and reported result groups are separate grains. Matching them is a resolution/verifier subject, not a join assumption; curation can override disputed matches.
  • Endpoint names from sources are evidence, not canonical endpoint truth. Canonical endpoint linkage is optional and accepted through resolution/verifier policy, with curation overrides for disputed mappings.
  • Eligibility is contextual: a trial can have multiple disease contexts, subtypes, stages, biomarker constraints, treatment-line constraints, and inclusion/exclusion polarity.
  • ChiCTR and ClinicalTrials.gov ids are external identifiers, not interchangeable canonical ids.

Legacy got this right in spirit but too imperatively: terms were gathered, searched, LLM-ranked, judged, then post-processed. The greenfield model should keep the cascade as resolution assets + verifier artifacts + optional curation override subjects, not as hidden workflow side effects (../bioloupe-data-gov/app/workflows/clinical_trial_eligibilities_workflow.rb).

Subject kindSource term examplesCandidate target typesPublishable basisHigh-impact?
trial_intervention_linkpembrolizumab, KEYTRUDA, MK-3475, chemo regimen, placebodrug spine resolved_drug_key; NCIt non-drug concept; combination/regimen; unresolvedexact synonym, cross-reference, context-verified, verifier-passed fuzzy/LLM, reviewed curation overrideyes
trial_disease_linkNSCLC, breast cancer, relapsed AMLcanonical Disease / NCIt disease conceptNCIt exact/cross-reference, context-verified, verifier-passed semantic/LLM, reviewed curation overrideyes
trial_biomarker_linkHER2-positive, EGFR exon 20 insertion, PD-L1 TPS ≥ 50%canonical Biomarker / gene/protein/variant conceptexact/cross-reference/context-verified or reviewed curation overrideyes
trial_endpoint_linkORR, PFS, OS, DOR, adverse event ratecanonical Endpoint catalogexact abbreviation mapping, accepted endpoint synonym, verifier-passed LLMmedium
trial_safety_ae_term_linkneutropenia, febrile neutropenia, hepatic toxicitycanonical adverse-event term / CTCAE-MedDRA-aligned conceptexact vocabulary mapping, source vocabulary, verifier-passed semantic/LLM, reviewed curation overridemedium/high for safety analytics
trial_org_linksponsor/collaborator free textcanonical Organisationexact legal name/alias, reviewed fuzzy override, reviewed curation overridemedium/high when used for ownership/sponsor analytics
trial_result_group_arm_linkCTGov result group label vs design arm titledesign armdeterministic id if source supplies; otherwise verifier or reviewed curation overridehigh for outcomes/SOC evidence
cross_registry_trial_linkNCT and ChiCTR records possibly same studycanonical trial identity clusterexact registry cross-reference, verifier, or reviewed curation overridehigh
flowchart LR
  Term[Extracted source term]
  Exact[Exact / synonym lookup]
  XRef[External-id or source cross-reference]
  Search[Semantic / lexical candidates]
  Judge[Independent verifier / judge]
  Basis[Accepted resolution basis]
  Accepted[Accepted canonical link]
  Conflict[Conflict / curation candidate]

  Term --> Exact
  Exact -- hit --> Basis
  Exact -- no hit --> XRef
  XRef -- hit --> Basis
  XRef -- no hit --> Search --> Judge --> Basis
  Basis -- accepted --> Accepted
  Basis -- ambiguous/rejected --> Conflict

Resolution outputs are candidate sets with explicit provenance basis; accepted links are derived from exact/cross-reference/context-verifier policy, not from curation by default. No fuzzy top hit auto-publishes. Curation enters only when a curator agent/human reviews an override or suppression under 05-agentic-curation-and-overrides.md.

Evidence claims are source-grounded records before canonical promotion. They should be narrow and replayable.

FieldMeaning
claim_idstable id generated from source snapshot + extractor version + claim path
trial_keysource-scoped trial key
source_locatorCTGov API URL + JSON path, or ChiCTR artifact/html locator
claim_kindarm, intervention_component, dose, route, schedule, investigational_component, sponsor_role
raw_texttext span or source field used
claim_valuestructured JSON value
extractor_versiondeterministic parser/LLM/prompt/schema version
requires_resolutionboolean + subject kind
FieldMeaning
trial_keysource-scoped trial key
polarityinclusion / exclusion / unknown
disease_context_termraw disease context if present
modifierdisease, subtype, extent, stage, status, risk, treatment_setting, prior_treatment_lines, treatment_lines, biomarker, age, ECOG
operatoreq, neq, gt, gte, lt, lte, range, textual
value_textsource-faithful textual value/span
value_numscalar numeric threshold; null for ranges
value_num_min, value_num_maxexplicit numeric bounds; both required for range, with the operator carrying inclusive/strict meaning
value_unityears, line, ECOG, or another source-grounded unit
evidence_textsource criterion sentence/span
inference_decision_id, inference_projection_revisionaccepted inference provenance; null for deterministic/legacy claims
resolution_subject_keyoptional key for disease/biomarker resolution

Legacy pressure: TrialEligibilityCriterion already separated operator/modifier/value/range/context and had modifiers for subtype, extent, stage, status, risk, treatment setting, treatment lines, and disease (../bioloupe-data-gov/app/models/trial_eligibility_criterion.rb).

FieldMeaning
trial_keysource-scoped trial key
source_artifact_keyimmutable CTGov/ChiCTR source snapshot key
source_locatorCTGov JSON path / ChiCTR artifact locator
source_result_group_keyCTGov result group id such as OG000 / EG000, or source-scoped ChiCTR group id if present
endpoint_source_keysource outcome/endpoint id or hash
measure_titlesource measure title
time_framesource time frame
statisticparam type/value/unit/dispersion/CI/p-value as available
populationanalyzed population / subgroup if present
arm_link_subject_keyoptional subject for result-group-to-design-arm resolution

The curation store defined in 05-agentic-curation-and-overrides.md is reused for reviewed corrections and overrides, not routine extraction/resolution. Clinical trials adds these high-value override subject kinds:

Subject kindWhy overrideableExample curation decision
trial_intervention_linkWrong drug links poison every downstream program view.MK-3475 in NCT… resolves to pembrolizumab/Drug X, basis exact synonym.”
trial_disease_linkTrial conditions and eligibility disease context drive disease pipelines.advanced NSCLC resolves to Non-Small Cell Lung Cancer, not generic Lung Cancer.”
trial_biomarker_linkBiomarker-positive eligibility changes treatment context.HER2 overexpressing maps to HER2-positive biomarker context.”
trial_endpoint_linkEndpoint normalization affects efficacy comparisons.objective response maps to ORR endpoint.”
trial_safety_ae_term_linkSafety-event term normalization affects safety comparisons and adverse-event analytics.febrile neutropenia resolves to the accepted AE term, not a generic neutropenia bucket.”
trial_result_group_arm_linkResult groups must align to the correct arm before outcome values are trusted.“CTGov result group B corresponds to study plan arm pembrolizumab + chemotherapy.”
cross_registry_trial_linkCross-registry duplicate merging changes canonical trial identity.“ChiCTR… and NCT… are / are not the same study.”
eligibility_modifier_assertionTreatment line/stage/subtype claims are often ambiguous.second-line or later becomes treatment_lines min=2.”

Curator agents may propose these changes, but high-impact overrides require human review. Verifier assets should handle the default long tail; curation exists for escalations, sampled audits, disputes, suppressions, and high-impact reversals.

Canonical clinical-trials assets should be pragmatic relational tables/materialized assets, not a formal ontology clone.

Canonical assetGrainBuilt from
canonical.trialsone canonical trial identityaccepted source trial snapshots + cross-registry links
canonical.trial_status_observationsone status/phase observation per retained source snapshotimmutable registry snapshots; capture time is not asserted effective time
canonical.trial_sitesone registry-listed sitesource location/contact records; contacts are not organisation identity
canonical.trial_design_armsone design armaccepted design-arm evidence claims
canonical.trial_intervention_componentsone intervention component in a design armaccepted intervention claims + resolution/verifier receipts
canonical.trial_conditionsone disease/condition assertion per trial/contextaccepted disease resolution/verifier receipts
canonical.trial_eligibility_assertionsone eligibility modifier assertioneligibility claims + resolution/verifier receipts; reviewed curation overlays when present
canonical.trial_endpointsone endpoint/measure assertionoutcome/endpoint claims + endpoint resolution/verifier receipts
canonical.trial_result_groupsone source result group, optionally linked to design armresult group claims + arm-link resolution/verifier receipts
canonical.trial_outcomesone statistic/measurement for endpoint/group/subgroupresult claims + contract checks
canonical.trial_safety_eventsone adverse-event term observation for one trial result group and event typeCTGov adverse-event rows + AE-term resolution + arm-link receipts
canonical.trial_safety_event_totalsone adverse-event total/denominator for one trial result group and event typeCTGov adverse-event group totals + result-group link receipts
canonical.trial_participant_flowone count per source result group/period/milestone or withdrawal reasonCTGov participant-flow module
canonical.trial_baseline_measurementsone baseline statistic per source result group/measure/categoryCTGov baseline-characteristics module
canonical.trial_organisation_rolesone sponsor/collaborator roleorganisation resolution/verifier receipts
canonical.trial_referencesone publication/reference linksource references + publication-link evidence

All tables below are read-only SQL models in the agent query layer. They must appear in generated q_catalog, and provenance-required rows/facts must resolve through q_provenance.

The seventeen models are one consistency family, not seventeen independently refreshed views. Dagster exposes one non-subsettable multi-asset. It freezes every checked-in SELECT, validates the complete candidate against schema/provenance/key constraints and owner spines, then atomically repoints every stable view plus the clinical_trials serving head. A failed candidate leaves the prior complete generation served; rollback also operates on the whole family. See 07 and 08 for the shared protocol.

Query modelGrainPurpose
q_trialsone canonical trialheadline trial search: registry ids, title, phase, status, start/completion dates, source coverage, sponsors, high-level disease/drug summary
q_trial_discovery_candidatesone discovered or rejected trial candidatediagnostics for why a trial entered/failed cohort filtering; owned by clinical trials, not the global search slice
q_trial_interventionsone intervention component per design armagent questions about drugs/regimens tested, monotherapy vs combination, investigational flag, dose/schedule summary
q_trial_design_armsone design armarm-level protocol design, arm type, title, description, component count, review status
q_trial_conditionsone disease/condition context per trialdisease-specific trial discovery and disease pipeline rollups
q_trial_eligibilityone eligibility assertionstructured inclusion/exclusion constraints for disease, stage, biomarker, prior therapy, line, ECOG/age
q_trial_endpointsone endpoint/measure assertionsource endpoint title/time frame + canonical endpoint link when accepted
q_trial_outcomesone endpoint/group/statistic rowarm-level or group-level reported efficacy/result-measure values with provenance
q_trial_safety_eventsone adverse-event term observation per trial/result group/event typearm-level or group-level CTGov safety counts with AE-term resolution
q_trial_safety_event_totalsone adverse-event total per trial/result group/event typedenominators and total affected counts for serious/other adverse event sections
q_trial_result_groupsone CTGov result group + accepted design-arm linkage statediagnostic/support surface for arm-link explainability; normal clinical questions use q_trial_outcomes / q_trial_safety_events / q_trial_design_arms (ADR-0005)
q_trial_status_historyone immutable registry status/phase observationtime-aware history behind the current q_trials projection
q_trial_sponsorsone sponsor/collaborator rolepreserve source org term and publish resolved_org_key only on accepted basis
q_trial_sitesone registry-listed siterecruitment status, geography, source contacts, and provenance
q_trial_referencesone registry-stated referencesource PMID/citation bridge; accepted publication identity/link remains owned by docs/23
q_trial_participant_flowone participant count per source group/period/milestone or withdrawal reasonparticipant disposition without flattening periods or result groups
q_trial_baseline_measurementsone baseline statistic per source group/measure/categorybaseline population context without collapsing result-group grain
q_provenance receipts, optionally exposed through q_trial_provenanceone or more receipts per provenance-required row/factglobal provenance back to source snapshots, extraction runs, evidence claims, resolution/verifier receipts, and curation overlays when applicable; q_trial_provenance may exist only as a convenience view over q_provenance
trial_key
primary_registry_source
primary_registry_id
nct_id
chictr_id
brief_title
official_title
phase
trial_status
study_type
start_date
primary_completion_date
completion_date
disease_names[]
resolved_disease_keys[]
intervention_names[]
resolved_drug_keys[]
sponsor_names[]
resolved_sponsor_keys[]
has_results
has_structured_eligibility
has_unresolved_high_impact_conflicts
source_freshness_at
provenance_key

resolved_trial_key(s) used by downstream slices (trial-event mentions in 24-slice-news-intelligence.md, trial-derived evidence in 23-slice-publications.md and 25-slice-standard-of-care.md) is defined here as a stable key into q_trials — the same identity as trial_key (e.g. trial:ctgov:NCT04380636). Downstream slices reference this key; they do not mint trial identity, and cross-registry duplicates still require exact cross-reference, verifier acceptance, or reviewed curation override before two registry keys collapse. resolved_sponsor_keys is not a separate identity: it is the set of resolved_org_key(s) from 26-slice-organisation-intelligence.md observed in a sponsor/collaborator role on this trial, so q_trials.resolved_sponsor_keys must resolve into the organisation spine rather than create a parallel sponsor namespace.

q_trial_outcomes comparative fact contract

Section titled “q_trial_outcomes comparative fact contract”

q_trial_outcomes is the clinical-trials side of the shared therapeutic-fact model in 20-pipeline-families.md. It is a registry/source-reported result fact, not a publication result and not a label indication. ClinicalTrials.gov v2 separates protocol modules from resultsSection.outcomeMeasuresModule, participantFlowModule, baselineCharacteristicsModule, and adverseEventsModule; the greenfield row grain follows that source pressure rather than collapsing everything into a wide trial row (ClinicalTrials.gov study data structure: https://clinicaltrials.gov/data-api/about-api/study-data-structure; OpenAPI: https://clinicaltrials.gov/api/oas/v2).

Contract fieldRequired meaning
PurposeAnswer: “What result was reported for this trial endpoint/group/subgroup, and why can I trust it?”
GrainOne reported statistic/measurement for one trial_key + source endpoint/outcome + source result group + population/subgroup + statistic kind.
Stable row keytrial_outcome:{trial_key}:{endpoint_source_key}:{source_result_group_key}:{population_hash}:{statistic_kind}.
Required columnstrial_outcome_key, trial_key, primary_registry_id, endpoint_source_key, endpoint_term, canonical_endpoint_key, source_result_group_key, design_arm_key, arm_link_state, population_label, population_n, statistic_kind, statistic_value, statistic_unit, dispersion_type, dispersion_value, ci_lower, ci_upper, p_value, time_frame, source_family, source_artifact_key, source_locator, provenance_key.
Search affordancesfilters on trial_key, registry id, canonical endpoint, intervention/drug keys via linked design arm, disease keys via trial context, has-results flag, statistic kind; trigram on endpoint/group labels.
Provenance ruleprovenance_grain=bundle for every row; field receipts for statistic_value, p_value, ci_lower, ci_upper, population_n, and arm_link_state when present.
Refresh/materializationimmutable table in the atomically promoted clinical_trials generation, served through stable query.q_trial_outcomes; the complete seventeen-model family moves only after canonical, endpoint, arm-link, provenance, and contract gates pass.
Blocking checksschema_contract, provenance_coverage, source_artifact_immutability, source_locator, resolution_basis, llm_verifier_required, curation_review_state when override applies.

Allowed fact_path examples:

trial_outcome:trial=NCT01234567:endpoint=orr:group=experimental:attr=statistic_value
trial_outcome:trial=NCT01234567:endpoint=pfs:group=control:attr=ci_lower
trial_outcome:trial=NCT01234567:endpoint=orr:group=experimental:attr=arm_link_state
trial_safety_event:trial=NCT01234567:event=neutropenia:event_type=serious:group=experimental:attr=subjects_affected
trial_safety_event_total:trial=NCT01234567:event_type=other:group=control:attr=subjects_at_risk

Publication rows may support or challenge this row, but they do not overwrite it. Cross-source comparisons happen through canonical assertions and provenance bundles, not by treating publication and registry statistics as the same row.

ColumnTypeNullabilitySemantic typeProvenance
trial_outcome_keytextnot nullquery row keyrow
trial_keytextnot nullcanonical trial keyrow
primary_registry_idtextnot nullsource-scoped external idrow
endpoint_source_keytextnot nullsource endpoint id/hashbundle
endpoint_termtextnot nullsource endpoint textbundle
canonical_endpoint_keytextnullableresolved endpoint keyfield when present
source_result_group_keytextnot nullsource result group keybundle
source_result_group_labeltextnullablesource result group labelbundle
design_arm_keytextnullablelinked protocol arm keyfield when present
arm_link_statetextnot nullresolution statefield
population_labeltextnullableanalyzed population/subgroupbundle when present
population_nnumericnullablecountfield when present
statistic_kindtextnot nulloutcome statistic kindbundle
statistic_valuenumericnullablenumeric resultfield when present
statistic_texttextnullablenon-numeric result textfield when present
statistic_unittextnullableunitfield when present
dispersion_typetextnullablesd/se/range/etcfield when present
dispersion_valuenumericnullabledispersion valuefield when present
ci_lowernumericnullableconfidence interval lowerfield when present
ci_uppernumericnullableconfidence interval upperfield when present
p_valuenumericnullablep-valuefield when present
time_frametextnullablesource time framebundle when present
source_familytextnot nullsource familyrow
source_artifact_keytextnot nullimmutable source artifactbundle
source_locatorjsonbnot nullsource locatorbundle
source_authority_tiertextnot nullauthority tierrow
assertion_statetextnot nulleffective assertion state; curation overlay if applicablerow
provenance_keyuuidnot nullprovenance bundlebundle

Catalog notes: arm_link_state allowed values should include source_exact, curation_override, verifier_accepted, unlinked, and conflict. Numeric fields are nullable because some registry outcomes are categorical/textual, but at least one of statistic_value or statistic_text must be present for a published row.

q_trial_outcomes vocab/search/example seed

Section titled “q_trial_outcomes vocab/search/example seed”

Legacy data-shape pressure from the local datalake confirms the query contract needs normalization rather than raw enum passthrough: result_outcomes.outcome_type contains PRIMARY, SECONDARY, OTHER_PRE_SPECIFIED, and POST_HOC; result/measurement param_type values include NUMBER, COUNT_OF_PARTICIPANTS, MEAN, MEDIAN, GEOMETRIC_MEAN, LEAST_SQUARES_MEAN, COUNT_OF_UNITS, and GEOMETRIC_LEAST_SQUARES_MEAN; dispersion values include standard deviation, standard error, confidence intervals, full range, and inter-quartile range. CTGov v2 is the source payload; these legacy AACT-derived values are compatibility pressure for the stable q_* vocabulary, not proof that AACT remains a source spine. The legacy tables also show why p_value, confidence intervals, and hazard/odds/risk-ratio style analysis fields must be field-provenanced rather than hidden in JSON (public.result_outcomes, public.outcome_measurements, public.outcome_analyses, sampled from local datalake).

Controlled vocabularies:

FieldControlled values / normalization
outcome_type if exposed laterprimary, secondary, other_pre_specified, post_hoc mapped from CTGov/legacy uppercase source values.
statistic_kindcount_of_participants, count_of_units, number, mean, median, geometric_mean, least_squares_mean, geometric_least_squares_mean, hazard_ratio, odds_ratio, risk_ratio, p_value, textual.
dispersion_typestandard_deviation, standard_error, confidence_interval_95, confidence_interval_90, confidence_interval_other, full_range, inter_quartile_range, geometric_coefficient_of_variation, other.
arm_link_statesource_exact, curation_override, verifier_accepted, unlinked, conflict.
source_authority_tierregistry_result. Protocol endpoint definitions live in q_trial_endpoints; derived statistics require a future versioned model rather than an unused enum slot.

Search/index contract:

Search needRequired index/affordance
Fetch all outcomes for one trialbtree on trial_key; btree on primary_registry_id.
Filter by endpoint/statisticbtree on canonical_endpoint_key, statistic_kind; trigram or full-text on endpoint_term.
Find unresolved arm linkspartial btree on arm_link_state where arm_link_state in ('unlinked','conflict').
Source/provenance auditbtree on source_family, source_artifact_key; hash/btree on provenance_key.
Numeric comparisonbtree on (canonical_endpoint_key, statistic_kind, statistic_value) where statistic_value is not null.
FieldRequired content
PurposeRegistry-reported adverse-event observations from ClinicalTrials.gov results.
GrainOne trial + source result group + event type + organ system + adverse-event term.
Stable row keytrial_safety_event_key (trial_safety_event:<trial_key>:<source_result_group_key>:<event_type>:<semantic_hash>), used by q_provenance.q_row_key; source locators stay in source_locator.
Required columnstrial_safety_event_key, trial_key, primary_registry_id, source_result_group_key, source_result_group_label, design_arm_key, arm_link_state, event_type, organ_system, adverse_event_term, resolved_ae_term_key, subjects_affected, subjects_at_risk, event_count, percentage, time_frame, assessment, vocab, source_family, source_artifact_key, source_locator, source_authority_tier, assertion_state, provenance_key.
Search affordancesbtree on trial_key, primary_registry_id, resolved_ae_term_key, event_type, source_result_group_key; trigram/full-text on adverse_event_term and organ_system; partial index on unresolved/conflict arm links.
Provenance ruleprovenance_grain=bundle for row identity and source locator; field receipts for subjects_affected, subjects_at_risk, event_count, percentage, and arm_link_state.
Blocking checksschema, provenance coverage, source artifact immutability, source locator, non-negative counts, subjects_affected <= subjects_at_risk when both present, percentage recomputation tolerance, result-group referential integrity, AE-term referential integrity when resolved.

event_type maps ClinicalTrials.gov serious/other sections to serious and other. Do not collapse that source seriousness section with grade_gte3: seriousness is an outcome/regulatory classification, while CTCAE grade is severity. The term and grade vocabularies come from 32; the source event section remains trial-owned.

FieldRequired content
PurposeRegistry-reported denominator/total rows for adverse-event sections.
GrainOne trial + source result group code + event type + classification.
Stable row keytrial_safety_event_total_key (trial_safety_event_total:<trial_key>:<source_result_group_key>:<event_type>:<semantic_hash>); source locators stay in source_locator.
Required columnstrial_safety_event_total_key, trial_key, primary_registry_id, source_result_group_key, source_result_group_label, design_arm_key, arm_link_state, event_type, classification, subjects_affected, subjects_at_risk, percentage, source_family, source_artifact_key, source_locator, source_authority_tier, assertion_state, provenance_key.
Provenance ruleField receipts for affected/risk/percentage and arm-link state.
Blocking checkssame count/range/source-locator gates as q_trial_safety_events; totals are not interchangeable with individual AE-term rows.

Example row shape:

trial_outcome_key: trial_outcome:trial:ctgov:NCT01234567:orr:grp-exp:overall:count_of_participants
trial_key: trial:ctgov:NCT01234567
primary_registry_id: NCT01234567
endpoint_term: Objective Response Rate
canonical_endpoint_key: endpoint:orr
source_result_group_key: ctgov_group:EG000
source_result_group_label: Experimental Arm
arm_link_state: verifier_accepted
population_label: Overall analyzed population
population_n: 84
statistic_kind: count_of_participants
statistic_value: 28
statistic_unit: Participants
time_frame: up to 24 months
source_family: ctgov
source_authority_tier: registry_result
provenance_key: <uuid>

Pathological examples the implementation must handle:

CaseWhy it is dangerousRequired behavior
Result group cannot be mapped to a protocol armCTGov result groups often do not have a safe one-to-one design arm link; forcing a match corrupts drug/arm analytics.Publish the outcome only with design_arm_key is null and arm_link_state='unlinked'; block any row that claims a design arm without accepted basis.
Same endpoint has numeric result and separate analysis rowMeasurements and analyses can carry different but related facts: count/percentage in measurements, HR/p-value/CI in analyses.Preserve separate field provenance for numeric measurement, HR/OR/RR, p-value, and CI; do not hide analyses inside one untraceable JSON blob.
Publication reports a different ORR from registryPublication evidence may update, refine, or conflict with registry result values.Keep registry result in q_trial_outcomes; publication value remains in q_publication_outcomes; optional comparison marks conflict without overwriting either.
Outcome is textual/categorical onlySome source outcomes are not numeric but are still meaningful.Allow statistic_text with statistic_value is null, but require source locator and statistic_kind='textual'.

Blocking checks:

  • every canonical.trials row has at least one source snapshot provenance pointer;
  • every provenance-required q_trials row/fact has at least one q_provenance receipt;
  • every resolved drug/disease/biomarker/org key in q_* has an accepted resolution/verifier receipt, allowed deterministic basis, or reviewed curation override;
  • every resolved_drug_key(s), resolved_disease_key(s), resolved_biomarker_key(s), canonical_endpoint_key, and resolved_sponsor_keys (org) value resolves to an active row in its owner q_* model (referential_integrity gate in 08-contracts-and-quality.md); resolved_sponsor_keys resolves through the organisation spine, not a local sponsor namespace;
  • no high-impact unresolved conflict publishes as accepted truth;
  • q_trial_outcomes rows preserve source group/endpoint/statistic identifiers and cannot be orphaned from trial + source snapshot;
  • result-group-to-design-arm links publish only if exact/source-provided, verifier-passed, or reviewed curation override;
  • schema shape matches q_catalog.

Warning checks:

  • source freshness drift;
  • unusual count deltas by source/partition;
  • high unresolved-candidate rate for a source run;
  • ChiCTR scraper degradation / parser drift;
  • LLM extraction output distribution shift.
  • Cadence: ClinicalTrials.gov API spine should refresh on a stable cron aligned to source update rhythm; legacy used Tuesday 08:00 UTC (../bioloupe-data-gov/docs/clinical-trials.md). Capture CTGov /version dataTimestamp with every run.
  • Partitioning: source snapshots should partition by source-native window/release where replayable. Canonical rebuilding may target trial keys, but the stable query family currently publishes as one complete non-subsettable generation.
  • Backfills: backfill by source/date partition and by trial-key subsets, not full global recompute.
  • Curation sensor: new reviewed override decisions for intervention/disease/biomarker/arm-link subjects trigger the owning canonical rebuild and then one complete atomic q_trials family publication.
  • Retries: narrow retries for transient CTGov/ChiCTR/network/LLM errors; no swallow-and-continue.
  • Eligibility inference: normal graph/bootstrap runs do not select evidence/trial_eligibility_gap_fill and make no provider calls. An operator selects it explicitly, enables it in run config, and uses max_requests plus optional trial_keys as the cohort boundary. The default is five requests and a retry-inclusive $10 preflight ceiling. Active task contract trial_eligibility_extract@v6 uses native structured output, exact declared-artifact quotes, requested-facet scope, deterministic section-bound polarity, quote-bound text/numbers, and projection trial-eligibility-projection-v6.
  • AACT reconciliation: optional, non-publishing check only. It may compare CTGov-derived counts/coverage to AACT samples, but failures create investigation alerts; they do not make AACT source authority.
  • ChiCTR: if retained in v1, scraper health metrics are first-class: zero-result anomaly, Cloudflare/anti-bot detection, parser drift, keyword coverage, S3/raw-artifact completeness (../bioloupe-data-gov/docs/chictr.md).
ops:
trial_eligibility_gap_fill:
config:
enabled: true
max_requests: 5
max_cost_usd: 10.0
trial_keys:
- trial:ctgov:NCT00000000

The asset still refuses any row whose latest reuse decision is not needs_fresh_llm. A current source-artifact change or projection-revision change creates new work identity; an accepted replay uses the inference cache and immutable projection receipt.

Provider activation additionally requires all checked-in eligibility semantic cases to pass the pinned active profile. Offline goldens currently prove validators and projection semantics only; no paid provider-backed eligibility evaluation has been run.

Minimum tests:

  1. sources.ctgov.trial_snapshots fixture uses real CTGov v2 shape and records API version, source data timestamp, source URL, content hash, and immutable artifact key.
  2. ChiCTR snapshot identity includes the full content hash, and normalization collapses only byte-identical observations with the same source version and capture time.
  3. CTGov discovery tests prove the source asset records accepted and rejected NCT candidates with query/filter/search-area provenance.
  4. normalized.trial_records preserves source ids and does not invent resolved entities.
  5. Trial identity key generation handles NCT and ChiCTR ids without collision.
  6. CTGov resultsSection.outcomeMeasuresModule parses outcome measurements and analyses into separate rows with field provenance for value, CI, p-value, and statistic kind.
  7. CTGov resultsSection.adverseEventsModule parses serious/other events plus event-group totals into q_trial_safety_events and q_trial_safety_event_totals.
  8. Intervention resolution exact synonym wins; fuzzy/semantic candidate without verifier does not publish.
  9. Disease subtype more-specific-than-base case is represented without losing base context (legacy had special subtype logic in ParticipationCriterion#all_diseases).
  10. Eligibility fixtures cover polarity, facets, scalar thresholds, explicit two-bound ranges, and rejected reversed/incomplete ranges.
  11. A FakeProvider plus local Postgres proves the enabled eligibility path, lifecycle receipts, accepted projection, cache replay, and no second provider call.
  12. Result group and design arm are not joined unless accepted by deterministic source evidence, verifier acceptance, or reviewed curation override.
  13. Curation supersession changes exactly the affected canonical/q_* rows.
  14. q_trials, q_trial_interventions, q_trial_eligibility, q_trial_outcomes, q_trial_safety_events, and q_trial_safety_event_totals satisfy generated q_catalog and required q_provenance contracts.
  15. Thin end-to-end fixture: one CTGov v2 NCT trial with two arms, one drug synonym, one disease, one biomarker eligibility, one endpoint, one result group, one safety event, one curation override.
  16. The coherent fixture resolves NCT04380636 to drug:pembrolizumab and disease:non-small-cell-lung-cancer-nsclc; a graph test proves trial resolution depends on the populated owner-name assets and never queries obsolete plural owner tables.

Concepts Promoted to 20-pipeline-families.md

Section titled “Concepts Promoted to 20-pipeline-families.md”

Stable cross-slice concepts discovered here:

  • Source-scoped external id — registry ids are namespaced; do not treat an NCT id and ChiCTR id as the same key type.
  • Protocol-vs-result grain separation — planned design arms and reported result groups are different things until linked.
  • Evidence claim before canonical assertion — source text/table rows become claims, then resolution/verifier state, then canonical facts; curation only overlays reviewed corrections.
  • Controlled vocabulary as anchor, not prison — NCIt/CDISC/MeSH guide modelling but do not force ontology-native storage.
  • ChiCTR: model as a first-class source identity now, but implement scraper hardening after the CTGov NCT spine unless product priority demands immediate operational parity. No NCT assumption may leak into canonical/query tables.
  • ClinicalTrials.gov history: capture forward history through immutable source snapshots, content hashes, and ops.ctgov_snapshot_diffs. Pre-existing historical archive ingestion is a separate source family decision, not an AACT fallback.
  • CDISC/SDTM: use as a naming and interchange reference only. Do not clone SDTM domains or force the q-layer into an SDTM-shaped schema.
  • AACT: use only for reconciliation, parity audits, or legacy coverage investigation. It must not be an upstream dependency for canonical.* or q_* facts.

The CTGov NCT spine implementation contract is now:

  1. Source mode is explicit: deterministic fixture mode for tests; opt-in live mode through ctgov_source Dagster resource run config. max_live_records may bound live smoke runs and must be reflected in materialization metadata.
  2. One Dagster multi_asset capture writes sources.ctgov.trial_discovery_runs and sources.ctgov.trial_discovery_candidates atomically; it never performs the search twice to materialize the two grains.
  3. Each discovery run records query params, CTGov API version, source data timestamp, source mode, complete/bounded capture boundary, max_live_records, deterministic scope-rule version, actual page count, and candidate count. Its key covers the query, boundary, upstream version, rule version, and capture attempt.
  4. Candidate selection deterministically requires an oncology/hematology source term, INTERVENTIONAL study type, and at least one supported therapeutic intervention type. Accepted and rejected rows retain the exact matched source term/path and stable reason code.
  5. Bounded smoke captures remain durable diagnostics but do not replace the latest complete cohort in q_trial_discovery_candidates and do not rewrite its assertion-provenance links.
  6. sources.ctgov.trial_snapshots stores immutable content-addressed v2 study JSON per accepted NCT id and registers source artifacts with hashes, URL, API version, source data timestamp, and retrieval metadata. A byte-different payload at the same CTGov source date creates a new row.
  7. ops.ctgov_snapshot_diffs records per-module hashes for protocol, eligibility, conditions, arms/interventions, outcome measures, adverse events, baseline, participant flow, sponsors, locations, and references.
  8. normalized.trial_records parses CTGov protocol modules into source-scoped trials, arms, interventions, sponsors, conditions, browse terms, and eligibility text.
  9. CTGov result parsers flatten outcomeMeasuresModule, participantFlowModule, baselineCharacteristicsModule, and adverseEventsModule into normalized result, participant-flow, baseline, safety-event, and safety-total records.
  10. Canonical/query publication preserves source-native grains and receipts across all seventeen models, including q_trial_sponsors, q_trial_sites, q_trial_references, q_trial_participant_flow, and q_trial_baseline_measurements; one validated generation promotes or rolls back as a unit, and publication identity remains owned by docs/23.
  11. evidence/trial_claim_tables exclusively owns shared trial-evidence DDL; deterministic, legacy-salvage, and inference writers depend on that asset.
  12. Eligibility provider work is opt-in and bounded. Only current-source needs_fresh_llm rows can execute, and only exact-quote, requested-facet, numerically coherent accepted outputs can be projected.
  13. q_trial_eligibility preserves disease context and resolved disease/biomarker keys, uses value_num only for scalar thresholds, and exposes both numeric bounds for ranges.