@inflexa-ai/harness 0.35.8 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/dist/auth/types.d.ts +5 -14
  2. package/dist/auth/types.js +3 -10
  3. package/dist/billing/usage-recorder.d.ts +1 -1
  4. package/dist/contracts/analogy-report.d.ts +1 -1
  5. package/dist/contracts/analogy-report.js +1 -1
  6. package/dist/contracts/chat-parts.d.ts +2 -3
  7. package/dist/contracts/index.d.ts +0 -4
  8. package/dist/contracts/index.js +0 -2
  9. package/dist/index.d.ts +0 -7
  10. package/dist/index.js +0 -21
  11. package/dist/lib/metrics.d.ts +1 -1
  12. package/dist/lib/otel-spans.d.ts +0 -2
  13. package/dist/lib/otel-spans.js +0 -2
  14. package/dist/loop/run-agent.d.ts +1 -1
  15. package/dist/runtime/assemble.d.ts +0 -3
  16. package/dist/runtime/assemble.js +0 -3
  17. package/dist/state/index.d.ts +0 -2
  18. package/dist/state/index.js +0 -1
  19. package/dist/state/init.js +1 -60
  20. package/dist/state/purge-analysis.d.ts +1 -2
  21. package/dist/state/purge-analysis.js +1 -2
  22. package/dist/tasks/regulatory-corpus-boot-refresh.js +1 -1
  23. package/dist/tools/bio/gene-preclinical-profile.d.ts +1 -3
  24. package/dist/tools/bio/gene-preclinical-profile.js +1 -3
  25. package/dist/tools/lib/chembl-client.d.ts +1 -4
  26. package/dist/tools/lib/chembl-client.js +1 -4
  27. package/dist/tools/lib/clinical-benchmarks-client.d.ts +1 -1
  28. package/dist/tools/lib/clinical-benchmarks-client.js +1 -1
  29. package/dist/tools/lib/clinical-trials-client.d.ts +0 -3
  30. package/dist/tools/lib/clinical-trials-client.js +0 -3
  31. package/dist/tools/lib/identifier-resolver.d.ts +5 -7
  32. package/dist/tools/lib/identifier-resolver.js +5 -7
  33. package/dist/tools/lib/iuphar-client.d.ts +1 -3
  34. package/dist/tools/lib/iuphar-client.js +1 -3
  35. package/dist/tools/lib/opentargets-client.d.ts +1 -1
  36. package/dist/tools/lib/opentargets-client.js +1 -1
  37. package/dist/tools/lib/protein-family-normalize.d.ts +5 -7
  38. package/dist/tools/lib/protein-family-normalize.js +5 -7
  39. package/dist/tools/lib/uniprot-client.d.ts +0 -7
  40. package/dist/tools/lib/uniprot-client.js +2 -9
  41. package/package.json +1 -1
  42. package/dist/contracts/target-assessment-row.d.ts +0 -85
  43. package/dist/contracts/target-assessment-row.js +0 -37
  44. package/dist/contracts/target-dossier.d.ts +0 -12423
  45. package/dist/contracts/target-dossier.js +0 -1282
  46. package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +0 -1
  47. package/dist/prompts/target-assessment/briefs/executive-recommendation.js +0 -85
  48. package/dist/prompts/target-assessment/briefs/index.d.ts +0 -4
  49. package/dist/prompts/target-assessment/briefs/index.js +0 -4
  50. package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +0 -1
  51. package/dist/prompts/target-assessment/briefs/liability-bullets.js +0 -114
  52. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +0 -1
  53. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +0 -37
  54. package/dist/prompts/target-assessment/briefs/translational-commentary.d.ts +0 -1
  55. package/dist/prompts/target-assessment/briefs/translational-commentary.js +0 -23
  56. package/dist/prompts/target-assessment/drugs-in-class.d.ts +0 -1
  57. package/dist/prompts/target-assessment/drugs-in-class.js +0 -29
  58. package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +0 -1
  59. package/dist/prompts/target-assessment/investigation/adversarial-critique.js +0 -67
  60. package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +0 -1
  61. package/dist/prompts/target-assessment/investigation/claim-reverification.js +0 -49
  62. package/dist/prompts/target-assessment/investigation/index.d.ts +0 -3
  63. package/dist/prompts/target-assessment/investigation/index.js +0 -3
  64. package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +0 -1
  65. package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +0 -49
  66. package/dist/prompts/target-assessment/modulator-triage.d.ts +0 -1
  67. package/dist/prompts/target-assessment/modulator-triage.js +0 -31
  68. package/dist/prompts/target-assessment/off-target-annotator.d.ts +0 -1
  69. package/dist/prompts/target-assessment/off-target-annotator.js +0 -28
  70. package/dist/prompts/target-assessment/tox-voice/core.d.ts +0 -1
  71. package/dist/prompts/target-assessment/tox-voice/core.js +0 -74
  72. package/dist/prompts/target-assessment/tox-voice/critique.d.ts +0 -7
  73. package/dist/prompts/target-assessment/tox-voice/critique.js +0 -109
  74. package/dist/prompts/target-assessment/tox-voice/exemplars.d.ts +0 -8
  75. package/dist/prompts/target-assessment/tox-voice/exemplars.js +0 -24
  76. package/dist/prompts/target-assessment/tox-voice/index.d.ts +0 -4
  77. package/dist/prompts/target-assessment/tox-voice/index.js +0 -4
  78. package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +0 -18
  79. package/dist/prompts/target-assessment/tox-voice/vocabulary.js +0 -61
  80. package/dist/state/target-assessments.d.ts +0 -68
  81. package/dist/state/target-assessments.js +0 -222
  82. package/dist/workflows/execute-target-assessment.d.ts +0 -118
  83. package/dist/workflows/execute-target-assessment.js +0 -631
  84. package/dist/workflows/target-assessment/assemblers/index.d.ts +0 -5
  85. package/dist/workflows/target-assessment/assemblers/index.js +0 -4
  86. package/dist/workflows/target-assessment/assemblers/literature.d.ts +0 -281
  87. package/dist/workflows/target-assessment/assemblers/literature.js +0 -971
  88. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +0 -118
  89. package/dist/workflows/target-assessment/assemblers/orchestrator.js +0 -945
  90. package/dist/workflows/target-assessment/assemblers/safety.d.ts +0 -374
  91. package/dist/workflows/target-assessment/assemblers/safety.js +0 -1068
  92. package/dist/workflows/target-assessment/assemblers/trials.d.ts +0 -30
  93. package/dist/workflows/target-assessment/assemblers/trials.js +0 -75
  94. package/dist/workflows/target-assessment/collectors/index.d.ts +0 -114
  95. package/dist/workflows/target-assessment/collectors/index.js +0 -538
  96. package/dist/workflows/target-assessment/coverage.d.ts +0 -97
  97. package/dist/workflows/target-assessment/coverage.js +0 -86
  98. package/dist/workflows/target-assessment/decisions/index.d.ts +0 -86
  99. package/dist/workflows/target-assessment/decisions/index.js +0 -127
  100. package/dist/workflows/target-assessment/fanout/index.d.ts +0 -32
  101. package/dist/workflows/target-assessment/fanout/index.js +0 -156
  102. package/dist/workflows/target-assessment/investigation/index.d.ts +0 -97
  103. package/dist/workflows/target-assessment/investigation/index.js +0 -419
  104. package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +0 -73
  105. package/dist/workflows/target-assessment/lib/approval-precedents.js +0 -185
  106. package/dist/workflows/target-assessment/lib/citation-validator.d.ts +0 -46
  107. package/dist/workflows/target-assessment/lib/citation-validator.js +0 -316
  108. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +0 -65
  109. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.js +0 -174
  110. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +0 -44
  111. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -152
  112. package/dist/workflows/target-assessment/lib/compute-derived.d.ts +0 -20
  113. package/dist/workflows/target-assessment/lib/compute-derived.js +0 -261
  114. package/dist/workflows/target-assessment/lib/compute-selectivity.d.ts +0 -15
  115. package/dist/workflows/target-assessment/lib/compute-selectivity.js +0 -18
  116. package/dist/workflows/target-assessment/lib/dedup-modulators.d.ts +0 -37
  117. package/dist/workflows/target-assessment/lib/dedup-modulators.js +0 -157
  118. package/dist/workflows/target-assessment/lib/evidence-conflict-detector.d.ts +0 -17
  119. package/dist/workflows/target-assessment/lib/evidence-conflict-detector.js +0 -100
  120. package/dist/workflows/target-assessment/lib/expression-constants.d.ts +0 -21
  121. package/dist/workflows/target-assessment/lib/expression-constants.js +0 -21
  122. package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +0 -20
  123. package/dist/workflows/target-assessment/lib/family-complex-supplement.js +0 -36
  124. package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +0 -72
  125. package/dist/workflows/target-assessment/lib/fda-label-safety.js +0 -166
  126. package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -30
  127. package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -44
  128. package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +0 -34
  129. package/dist/workflows/target-assessment/lib/hpo-organ-map.js +0 -63
  130. package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +0 -19
  131. package/dist/workflows/target-assessment/lib/impc-organ-map.js +0 -39
  132. package/dist/workflows/target-assessment/lib/intended-polypharm-filter.d.ts +0 -4
  133. package/dist/workflows/target-assessment/lib/intended-polypharm-filter.js +0 -24
  134. package/dist/workflows/target-assessment/lib/llm-step.d.ts +0 -80
  135. package/dist/workflows/target-assessment/lib/llm-step.js +0 -101
  136. package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +0 -40
  137. package/dist/workflows/target-assessment/lib/meddra-organ-map.js +0 -138
  138. package/dist/workflows/target-assessment/lib/recommendation-quality-gates.d.ts +0 -17
  139. package/dist/workflows/target-assessment/lib/recommendation-quality-gates.js +0 -99
  140. package/dist/workflows/target-assessment/lib/recommendation-sanitizer.d.ts +0 -2
  141. package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +0 -51
  142. package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +0 -25
  143. package/dist/workflows/target-assessment/lib/regulatory-actions.js +0 -176
  144. package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +0 -44
  145. package/dist/workflows/target-assessment/lib/safety-corroboration.js +0 -273
  146. package/dist/workflows/target-assessment/lib/structured-llm.d.ts +0 -61
  147. package/dist/workflows/target-assessment/lib/structured-llm.js +0 -102
  148. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +0 -59
  149. package/dist/workflows/target-assessment/lib/target-identity-filter.js +0 -95
  150. package/dist/workflows/target-assessment/lib/therapeutic-programs.d.ts +0 -20
  151. package/dist/workflows/target-assessment/lib/therapeutic-programs.js +0 -68
  152. package/dist/workflows/target-assessment/metrics.d.ts +0 -16
  153. package/dist/workflows/target-assessment/metrics.js +0 -30
  154. package/dist/workflows/target-assessment/phase4-assemble.d.ts +0 -3182
  155. package/dist/workflows/target-assessment/phase4-assemble.js +0 -39
  156. package/dist/workflows/target-assessment/phase5-persist.d.ts +0 -59
  157. package/dist/workflows/target-assessment/phase5-persist.js +0 -349
  158. package/dist/workflows/target-assessment/progress-stream.d.ts +0 -75
  159. package/dist/workflows/target-assessment/progress-stream.js +0 -110
  160. package/dist/workflows/target-assessment/progress.d.ts +0 -39
  161. package/dist/workflows/target-assessment/progress.js +0 -77
  162. package/dist/workflows/target-assessment/schemas.d.ts +0 -970
  163. package/dist/workflows/target-assessment/schemas.js +0 -318
  164. package/dist/workflows/target-assessment/steps/collectors/pathways.d.ts +0 -15
  165. package/dist/workflows/target-assessment/steps/collectors/pathways.js +0 -58
  166. package/dist/workflows/target-assessment/steps/fanout/aes-for-one-class-drug-step.d.ts +0 -34
  167. package/dist/workflows/target-assessment/steps/fanout/aes-for-one-class-drug-step.js +0 -31
  168. package/dist/workflows/target-assessment/steps/fanout/aes-for-one-trial-step.d.ts +0 -57
  169. package/dist/workflows/target-assessment/steps/fanout/aes-for-one-trial-step.js +0 -54
  170. package/dist/workflows/target-assessment/steps/fanout/faers-for-one-modulator-step.d.ts +0 -35
  171. package/dist/workflows/target-assessment/steps/fanout/faers-for-one-modulator-step.js +0 -32
  172. package/dist/workflows/target-assessment/steps/fanout/index.d.ts +0 -14
  173. package/dist/workflows/target-assessment/steps/fanout/index.js +0 -10
  174. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +0 -29
  175. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +0 -29
  176. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +0 -657
  177. package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -34
  178. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +0 -806
  179. package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -16
  180. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -28
  181. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -27
  182. package/dist/workflows/target-assessment/sub-workflows/index.d.ts +0 -15
  183. package/dist/workflows/target-assessment/sub-workflows/index.js +0 -11
  184. package/dist/workflows/target-assessment/sub-workflows/per-class-drug-aes.d.ts +0 -41
  185. package/dist/workflows/target-assessment/sub-workflows/per-class-drug-aes.js +0 -12
  186. package/dist/workflows/target-assessment/sub-workflows/per-modulator-faers.d.ts +0 -41
  187. package/dist/workflows/target-assessment/sub-workflows/per-modulator-faers.js +0 -12
  188. package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +0 -37
  189. package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.js +0 -12
  190. package/dist/workflows/target-assessment/sub-workflows/per-trial-aes.d.ts +0 -66
  191. package/dist/workflows/target-assessment/sub-workflows/per-trial-aes.js +0 -12
  192. package/dist/workflows/target-assessment/synthesis/index.d.ts +0 -607
  193. package/dist/workflows/target-assessment/synthesis/index.js +0 -354
  194. package/dist/workflows/target-assessment/synthesis-probes.d.ts +0 -56
  195. package/dist/workflows/target-assessment/synthesis-probes.js +0 -320
@@ -1,945 +0,0 @@
1
- /**
2
- * Phase-4 deterministic assemblers.
3
- *
4
- * Each assembler maps Phase-1 / Phase-2 / Phase-3 outputs onto the
5
- * corresponding dossier section schema. Coverage is honored end-to-end: an
6
- * upstream that came back empty or was never consulted yields
7
- * `queried_no_data` / `not_loaded` rather than a fabricated section.
8
- *
9
- * Where a section applies a threshold of its own, the decision runs through
10
- * `coverageFromFilteredRows` over both counts — what survived and what the
11
- * threshold discarded — so a section our own filter emptied reports
12
- * `filtered` rather than an `available` section holding nothing. Deciding
13
- * coverage first and filtering the payload afterwards is the shape that loses
14
- * that distinction.
15
- *
16
- * Aggregate rows preserve their contributing evidence under `evidence: [...]`
17
- * arrays.
18
- */
19
- import { classifyClinicalEvidenceTrial, } from "../lib/clinical-evidence-attribution.js";
20
- import { inferTherapeuticArea, getBenchmarks, getDatasetAttribution } from "../../../tools/lib/clinical-benchmarks-client.js";
21
- import { inferModalityFromFamily } from "../../../tools/lib/protein-family-modality.js";
22
- import { resolveFamilySiblingUniprots, resolveOnTargetChemblIds } from "../lib/target-identity-filter.js";
23
- import { getDrugPrimaryTargetUniprots } from "../../../tools/lib/chembl-client.js";
24
- import { annotateOffTargetPanel } from "../lib/clinical-consequence-annotator.js";
25
- import { coverageFromFilteredRows, coverageFromRows } from "../coverage.js";
26
- import { toOrganSystems } from "../lib/impc-organ-map.js";
27
- import { fetchRegulatoryActions } from "../lib/regulatory-actions.js";
28
- import { HIGH_EXPRESSION_TPM_THRESHOLD } from "../lib/expression-constants.js";
29
- export { HIGH_EXPRESSION_TPM_THRESHOLD };
30
- import { resolveModulatorMoleculeType } from "../lib/dedup-modulators.js";
31
- import { searchFailedTrialsForDrugNames } from "../../../tools/lib/clinical-trials-client.js";
32
- import { aggregateFaersAcrossModulators, aggregateTrialAes, aggregateOffTargetPanel, aggregateClassPrecedent, buildOrganRollup, assembleDrugInteractions, assembleLiabilitySummary, claimSupportEvidence, meetsTpmFloor, isSafetyRelevant, resolveTissueOrganSystem, } from "./safety.js";
33
- import { assemblePreclinicalLiterature, assembleKoSupportingLiterature, tallyInternalReferences, assembleKeyPapers, assembleEvidenceTimeline, assembleTranslationalChain, assembleMolecularInteractions, assembleBiomarkerPotential, assembleResistanceEvidence, assembleCombinationEvidence, assembleEvidenceConflicts, assembleAdditionalEvidence, isSelfReference, isClinicalMeasurement, indicationCompositeScore, partitionTrialsByAttribution, classifyClinicalTrialConfidence, assembleDiscoveryTrials, dedupePpiPartners, } from "./literature.js";
34
- import { buildTrialOutcomeFilter, aggregateTrialOutcomes } from "./trials.js";
35
- import { buildFailureCategoryDiscriminated } from "./literature.js";
36
- const NOT_LOADED_PHASE5 = "Phase 5 synthesis not yet implemented";
37
- /** What emptied `clinical_development.trials` when nothing survived. */
38
- const ACTIVE_TRIALS_FILTER = "text match confidence below medium, attribution not eligible for toxicology aggregation, or attributed to a related family receptor";
39
- /** What emptied the two failed-trial sections when nothing survived. */
40
- const FAILED_TRIALS_FILTER = "attribution not eligible for toxicology aggregation, or attributed to a related family receptor";
41
- /** What emptied the two association sections when nothing survived. */
42
- const INDICATIONS_FILTER = "association restates the target itself, names a clinical measurement rather than a disease, or carries no scored evidence source";
43
- /** Sections the workflow body stamps between assembly and synthesis. */
44
- const NOT_LOADED_POST_ASSEMBLY = "assembled after Phase 4, from the segmented regulatory label signals";
45
- // Order matches evidence-priority rank: genetic > known_drug > animal_model > somatic > literature.
46
- function makeAssociationEvidence(a) {
47
- const out = [];
48
- if (a.geneticAssociationScore != null) {
49
- out.push({
50
- source: "opentargets:genetic_association",
51
- score: a.geneticAssociationScore,
52
- predicate: "associated_with",
53
- is_human: true,
54
- metadata: { disease_id: a.diseaseId },
55
- });
56
- }
57
- if (a.knownDrugScore != null) {
58
- out.push({
59
- source: "opentargets:known_drug",
60
- score: a.knownDrugScore,
61
- predicate: "therapeutic_target",
62
- is_human: true,
63
- is_clinical: true,
64
- metadata: { disease_id: a.diseaseId },
65
- });
66
- }
67
- if (a.animalModelScore != null) {
68
- out.push({
69
- source: "opentargets:animal_model",
70
- score: a.animalModelScore,
71
- predicate: "validated_in_animal_model",
72
- metadata: { disease_id: a.diseaseId },
73
- });
74
- }
75
- if (a.somaticMutationScore != null) {
76
- out.push({
77
- source: "opentargets:somatic_mutation",
78
- score: a.somaticMutationScore,
79
- predicate: "somatic_mutation_associated",
80
- is_human: true,
81
- metadata: { disease_id: a.diseaseId },
82
- });
83
- }
84
- if (a.literatureScore != null) {
85
- out.push({
86
- source: "opentargets:literature",
87
- score: a.literatureScore,
88
- predicate: "literature_co_occurrence",
89
- metadata: { disease_id: a.diseaseId },
90
- });
91
- }
92
- return out;
93
- }
94
- // ── Section assemblers ───────────────────────────────────────────────
95
- export function assembleEntity(resolved) {
96
- return {
97
- canonicalId: resolved.canonicalId,
98
- symbol: resolved.geneSymbol,
99
- displayName: resolved.approvedName,
100
- entityType: "gene",
101
- ontology: resolved.canonicalOntology === "hgnc" ? "HGNC" : "Ensembl",
102
- identifiers: {
103
- hgnc: resolved.ids.hgnc ?? undefined,
104
- ensembl: resolved.ids.ensembl ?? undefined,
105
- uniprot: resolved.ids.uniprot ?? undefined,
106
- chembl: resolved.ids.chembl ?? undefined,
107
- ncbiGene: resolved.ids.entrez ?? undefined,
108
- },
109
- synonyms: resolved.synonyms,
110
- proteinFamily: resolved.proteinFamily ?? undefined,
111
- };
112
- }
113
- // Maps ChEMBL molecule_type (lowercase) to the tractability modality bucket.
114
- const MOLECULE_TYPE_TO_MODALITY = {
115
- "small molecule": "small_molecule",
116
- antibody: "antibody",
117
- "antibody-drug conjugate": "antibody",
118
- protein: "other_clinical",
119
- peptide: "other_clinical",
120
- enzyme: "other_clinical",
121
- };
122
- function normalizeDrugMoleculeType(drug) {
123
- const name = (drug.drug_name ?? "").trim().toUpperCase();
124
- // SMC021 is oral salmon calcitonin, not a non-peptide small molecule. Some
125
- // source paths can describe the formulation as oral and accidentally leave a
126
- // small-molecule annotation downstream; keep the therapeutic substance class.
127
- if (name === "SMC021" || name.includes("SALMON CALCITONIN") || name.includes("CALCITONIN SALMON")) {
128
- return "Peptide";
129
- }
130
- return drug.molecule_type;
131
- }
132
- /**
133
- * Pure helper that flips `has_approved_drug=true` and populates
134
- * `approved_drug_ids` on any modality that has a max_phase=4 drug in
135
- * the provided list. Unrecognised molecule_types are silently ignored.
136
- */
137
- export function enrichTractabilityModalitiesWithApprovals(modalities, drugs) {
138
- const approvedByModality = new Map();
139
- for (const d of drugs) {
140
- if ((d.max_phase ?? 0) < 4)
141
- continue;
142
- const mod = MOLECULE_TYPE_TO_MODALITY[(normalizeDrugMoleculeType(d) ?? "").toLowerCase()];
143
- if (!mod)
144
- continue;
145
- const arr = approvedByModality.get(mod) ?? [];
146
- arr.push(d.drug_id);
147
- approvedByModality.set(mod, arr);
148
- }
149
- return modalities.map((m) => {
150
- const ids = approvedByModality.get(m.modality);
151
- if (!ids || ids.length === 0)
152
- return m;
153
- return { ...m, has_approved_drug: true, approved_drug_ids: [...new Set(ids)].sort() };
154
- });
155
- }
156
- export function assembleTractability(resolved, ot, drugs) {
157
- const familyFallback = () => {
158
- const fallback = inferModalityFromFamily(resolved.proteinFamily);
159
- return {
160
- coverage: "available",
161
- data: {
162
- modalities: [
163
- {
164
- modality: "small_molecule",
165
- levels: [],
166
- has_approved_drug: false,
167
- has_clinical_stage: false,
168
- is_inferred_from_family: true,
169
- },
170
- ],
171
- preferred_modality: fallback.preferred_modality,
172
- drug_molecule_types_present: [],
173
- },
174
- inference_path: `family-fallback (${fallback.source}: ${fallback.family ?? "default"}) — ${fallback.rationale}`,
175
- };
176
- };
177
- if (ot.coverage !== "available")
178
- return familyFallback();
179
- const t = ot.data.tractability;
180
- if (!t)
181
- return familyFallback();
182
- const modalities = [
183
- {
184
- modality: "small_molecule",
185
- levels: t.smallMolecule ? ["validated"] : [],
186
- has_approved_drug: false,
187
- has_clinical_stage: !!t.smallMolecule,
188
- is_inferred_from_family: false,
189
- },
190
- {
191
- modality: "antibody",
192
- levels: t.antibody ? ["validated"] : [],
193
- has_approved_drug: false,
194
- has_clinical_stage: !!t.antibody,
195
- is_inferred_from_family: false,
196
- },
197
- {
198
- modality: "other_clinical",
199
- levels: t.otherModalities ? ["validated"] : [],
200
- has_approved_drug: false,
201
- has_clinical_stage: !!t.otherModalities,
202
- is_inferred_from_family: false,
203
- },
204
- ];
205
- const preferred = t.smallMolecule ? "small_molecule" : t.antibody ? "antibody" : t.otherModalities ? "other_clinical" : null;
206
- const modalitiesEnriched = enrichTractabilityModalitiesWithApprovals(modalities, drugs);
207
- // Collect molecule types from approved/clinical drugs for the tractability field.
208
- const drugMoleculeTypes = [...new Set(drugs.map((d) => d.molecule_type).filter((m) => !!m))];
209
- return {
210
- coverage: "available",
211
- data: {
212
- modalities: modalitiesEnriched,
213
- preferred_modality: preferred,
214
- drug_molecule_types_present: drugMoleculeTypes,
215
- },
216
- };
217
- }
218
- // ── Top-level dossier assembly ──────────────────────────────────────
219
- /**
220
- * Build a body-only dossier from Phase-2 (and optionally Phase-3)
221
- * outputs. Synthesis-dependent fields stay `not_loaded` until Phase 5
222
- * stamps them in `phase5-persist.ts`. The derived sub-tree is NOT
223
- * attached here — phase5-persist computes it after synthesis is stamped
224
- * so the final derived values reflect liability bullets and safety flags.
225
- */
226
- export async function assembleDossier(pool, phase2, phase3, annotatorDeps, logger) {
227
- const fanout = phase3?.fanout;
228
- const faersAgg = aggregateFaersAcrossModulators(fanout);
229
- const trialAesAgg = aggregateTrialAes(fanout);
230
- // Off-target panel needs the assessment UniProt and gene symbol: UniProt to
231
- // recognise alternate ChEMBL IDs as self-hits, gene symbol to detect
232
- // obligate-cofactor heterodimers (e.g., CALCR/RAMP3 = AMY3 for CALCR).
233
- const assessmentUniprot = phase2.phase1.resolved.ids.uniprot ?? "";
234
- const geneSymbol = phase2.phase1.resolved.geneSymbol ?? "";
235
- const familyComplexesCollector = phase2.phase1.collectors.familyComplexes;
236
- const familyComplexesBundle = familyComplexesCollector.coverage === "available" ? familyComplexesCollector.data : null;
237
- const [onTargetChemblIds, familySiblingUniprots] = await Promise.all([
238
- resolveOnTargetChemblIds(assessmentUniprot, logger),
239
- resolveFamilySiblingUniprots(assessmentUniprot || geneSymbol, logger),
240
- ]);
241
- const offTargetPanel = aggregateOffTargetPanel(phase2, fanout, assessmentUniprot, geneSymbol, familyComplexesBundle, onTargetChemblIds);
242
- if (offTargetPanel) {
243
- await annotateOffTargetPanel(pool, offTargetPanel, geneSymbol, annotatorDeps);
244
- }
245
- const offTargetRows = offTargetPanel?.rows ?? null;
246
- const classP = aggregateClassPrecedent(phase2, fanout);
247
- // Enrich modulator metadata before any downstream assembly so that
248
- // tractability, drug_interactions, and class-precedent all see the same
249
- // PubChem-refined molecule_type for entries that ChEMBL labelled "Unknown".
250
- const chemblModsCollector = phase2.phase1.collectors.chemblModulators;
251
- const enrichedModulators = chemblModsCollector.coverage === "available" ? await resolveModulatorMoleculeType(chemblModsCollector.data.modulators) : [];
252
- // drugInteractionRows must be computed before buildOrganRollup so we can
253
- // derive the drug IDs for regulatory_actions lookup and pass them in.
254
- const drugInteractionRows = assembleDrugInteractions(phase2, enrichedModulators);
255
- const regulatoryActionRows = await fetchRegulatoryActions((drugInteractionRows ?? []).map((d) => ({ chemblId: d.drug_id, name: d.drug_name })));
256
- const organRollupRows = buildOrganRollup(faersAgg, trialAesAgg, offTargetRows, classP, regulatoryActionRows.length > 0 ? regulatoryActionRows : null);
257
- const preclinicalLitData = assemblePreclinicalLiterature(phase2);
258
- const koSupportingLiterature = assembleKoSupportingLiterature(phase2);
259
- const internalRefCounts = tallyInternalReferences([
260
- koSupportingLiterature,
261
- ...(organRollupRows ?? []).map((r) => r.evidence),
262
- ...(offTargetRows ?? []).map((r) => claimSupportEvidence(r.support)),
263
- ]);
264
- const keyPapersRows = assembleKeyPapers(phase2, internalRefCounts);
265
- const evidenceTimeline = assembleEvidenceTimeline(phase2);
266
- const translationalChain = assembleTranslationalChain(phase2);
267
- const attrCtx = {
268
- assessmentUniprot,
269
- assessmentSymbol: phase2.phase1.resolved.geneSymbol,
270
- familyUniprots: familySiblingUniprots,
271
- drugTargetResolver: getDrugPrimaryTargetUniprots,
272
- };
273
- // Derive known class drug names from ChEMBL modulators (uppercase) for
274
- // drug_in_class_match classification inside assembleDiscoveryTrials.
275
- const knownClassDrugNames = new Set((drugInteractionRows ?? []).map((r) => r.drug_name.toUpperCase()));
276
- // Build the attribution filter for aggregateTrialOutcomes by partitioning
277
- // all ctgov trials (active + failed) with the same attribution logic used
278
- // for clinical_development.trials. This drops erenumab/paralog trials from
279
- // clinical_development.outcomes when their ChEMBL IDs resolve to a related
280
- // receptor rather than the assessment target.
281
- const trialOutcomeFilter = await buildTrialOutcomeFilter(phase2, attrCtx, knownClassDrugNames, phase2.phase1.resolved.geneSymbol);
282
- const trialOutcomesRows = aggregateTrialOutcomes(fanout, trialOutcomeFilter);
283
- const resolved = phase2.phase1.resolved;
284
- const ot = phase2.phase1.collectors.opentargets;
285
- const ctgov = phase2.phase1.collectors.ctgov;
286
- const clinvar = phase2.phase1.collectors.clinvar;
287
- const cbioportal = phase2.phase1.collectors.cbioportal;
288
- const expression = phase2.phase1.collectors.expressionHuman;
289
- const expressionMulti = phase2.phase1.collectors.expressionMultiSpecies;
290
- const impc = phase2.phase1.collectors.impc;
291
- const pubmed = phase2.phase1.collectors.pubmedIndex;
292
- const pathways = phase2.phase1.collectors.pathways;
293
- const ppi = phase2.phase1.collectors.stringPpi;
294
- // Every trial row carries its attribution from assembly onward: the
295
- // evidence role decides whether a row supports the target or is context.
296
- const knownClassDrugs = await Promise.all((drugInteractionRows ?? []).map(async (d) => {
297
- const resolvedTargets = d.drug_id ? await getDrugPrimaryTargetUniprots(d.drug_id).catch(() => []) : [];
298
- return {
299
- name: d.drug_name,
300
- moleculeChemblId: d.drug_id,
301
- targetUniprots: resolvedTargets.length > 0 ? resolvedTargets : assessmentUniprot ? [assessmentUniprot] : [],
302
- };
303
- }));
304
- const programsCollector = phase2.phase1.collectors.therapeuticPrograms;
305
- const therapeuticPrograms = programsCollector?.coverage === "available"
306
- ? programsCollector.data.programs.map((program) => ({
307
- programId: program.programId,
308
- name: program.name,
309
- targetSymbol: program.targetSymbol,
310
- targetUniprot: program.targetUniprot,
311
- modality: program.modality,
312
- nctIds: program.nctIds,
313
- }))
314
- : [];
315
- const evidenceAttribution = {
316
- assessmentSymbol: geneSymbol,
317
- assessmentUniprot,
318
- familyUniprots: familySiblingUniprots,
319
- knownClassDrugs,
320
- therapeuticPrograms,
321
- };
322
- const trialSourceByNct = new Map();
323
- if (ctgov.coverage === "available") {
324
- for (const t of [...ctgov.data.active, ...ctgov.data.failed])
325
- trialSourceByNct.set(t.nctId, t);
326
- }
327
- const attachTrialAttribution = (row) => {
328
- const source = trialSourceByNct.get(row.nct_id);
329
- const classified = source
330
- ? classifyClinicalEvidenceTrial(source, evidenceAttribution)
331
- : {
332
- attribution: {
333
- relationship: "unknown",
334
- evidence_role: "excluded",
335
- basis: [{ kind: "text_match", source: "ctgov" }],
336
- resolved_interventions: [],
337
- exclusion_reason: "no CT.gov record was available to attribute this trial to the assessed target",
338
- },
339
- eligible_for_toxicology_aggregation: false,
340
- };
341
- return {
342
- ...row,
343
- attribution: classified.attribution,
344
- eligible_for_toxicology_aggregation: classified.eligible_for_toxicology_aggregation,
345
- };
346
- };
347
- const discoveryTrials = await assembleDiscoveryTrials(phase2, phase2.phase1.resolved.geneSymbol, attrCtx, knownClassDrugNames, attachTrialAttribution);
348
- const indicationsRaw = ot.coverage === "available" ? ot.data.associations : [];
349
- const filteredIndications = indicationsRaw.filter((a) => !isSelfReference(resolved.canonicalId, a.diseaseId, resolved.geneSymbol, a.diseaseName) && !isClinicalMeasurement(a.diseaseName));
350
- const ta = inferTherapeuticArea(filteredIndications.map((a) => a.diseaseName));
351
- const benchmarks = getBenchmarks(ta);
352
- const datasetAttr = getDatasetAttribution();
353
- const offTissueCandidates = expression.coverage === "available"
354
- ? expression.data.tissues.filter((t) => meetsTpmFloor(t.tissueLabel, t.value ?? 0) && isSafetyRelevant(t.tissueLabel, t.organSystem))
355
- : [];
356
- // A tissue whose anatomy does not resolve onto an organ system is dropped
357
- // rather than filed under a neighbouring organ; the count is reported so
358
- // the section does not overstate its own completeness.
359
- const offTissueRows = offTissueCandidates.flatMap((t) => {
360
- const organ = resolveTissueOrganSystem(t.tissueLabel, t.organSystem);
361
- if (!organ)
362
- return [];
363
- const support = {
364
- state: "unknown",
365
- reason: `${t.tissueLabel} expression is a measured value from the expression atlas, not a citable per-tissue record`,
366
- };
367
- return [{ tissue: t.tissueLabel, organ, tpm: t.value ?? 0, support }];
368
- });
369
- const offTissueDroppedCount = offTissueCandidates.length - offTissueRows.length;
370
- // Candidates that all failed to resolve leave the section empty for a
371
- // reason of our own making, not because the atlas held nothing.
372
- const offTissueSection = expression.coverage !== "available"
373
- ? { coverage: "queried_no_data" }
374
- : offTissueRows.length > 0
375
- ? {
376
- coverage: "available",
377
- data: { rows: offTissueRows },
378
- ...(offTissueDroppedCount > 0 ? { dropped_count: offTissueDroppedCount } : {}),
379
- }
380
- : offTissueDroppedCount > 0
381
- ? {
382
- coverage: "filtered",
383
- filter: "tissue anatomy did not resolve onto an organ system",
384
- dropped_count: offTissueDroppedCount,
385
- }
386
- : { coverage: "queried_no_data" };
387
- const drugsForTractability = enrichedModulators.map((m) => ({
388
- drug_id: m.moleculeChemblId,
389
- drug_name: m.preferredName,
390
- molecule_type: m.moleculeType,
391
- max_phase: m.maxPhase,
392
- }));
393
- const tractabilitySection = assembleTractability(resolved, ot, drugsForTractability);
394
- const preferredModality = tractabilitySection.coverage === "available" ? tractabilitySection.data.preferred_modality : null;
395
- const liabilitySummary = assembleLiabilitySummary(phase2, {
396
- offTargetRows,
397
- preferredModality,
398
- });
399
- const allRowCandidates = filteredIndications.slice(0, 50).map((a) => {
400
- const sources = [
401
- a.geneticAssociationScore != null ? "ot_genetics" : null,
402
- a.knownDrugScore != null ? "ot_known_drug" : null,
403
- a.animalModelScore != null ? "ot_animal_model" : null,
404
- a.somaticMutationScore != null ? "ot_somatic_mutation" : null,
405
- a.literatureScore != null ? "literature" : null,
406
- ].filter((s) => s !== null);
407
- const evidence = makeAssociationEvidence(a);
408
- return { a, sources, evidence };
409
- });
410
- const supportedCandidates = allRowCandidates.filter(({ sources, evidence }) => sources.length > 0 || evidence.length > 0);
411
- const unsupportedCandidates = allRowCandidates.filter(({ sources, evidence }) => sources.length === 0 && evidence.length === 0);
412
- const indicationRows = supportedCandidates.map(({ a, sources, evidence }) => {
413
- const uniquePaperCount = a.literaturePmids?.length ?? 0;
414
- const { composite, breakdown } = indicationCompositeScore(a.score, sources, uniquePaperCount);
415
- return {
416
- disease_id: a.diseaseId,
417
- disease_name: a.diseaseName,
418
- composite_score: composite,
419
- composite_score_breakdown: breakdown,
420
- evidence_score: a.score,
421
- source_count: sources.length,
422
- unique_paper_count: uniquePaperCount,
423
- sources,
424
- evidence,
425
- };
426
- });
427
- // Run partitionTrialsByAttribution on active trials first to detect off-target
428
- // before building clinical_development.trials and discovery_trials sections.
429
- // Since ctgov interventions are plain strings (no ChEMBL IDs), the resolver
430
- // cannot fire and all trials land in primary — off-target detection activates
431
- // only when ChEMBL IDs are available downstream.
432
- const activeTrialsPartitioned = ctgov.coverage === "available" ? await partitionTrialsByAttribution(ctgov.data.active, attrCtx) : null;
433
- // Everything CT.gov offered before any threshold of ours ran — the baseline
434
- // the trials section measures its own drops against.
435
- const activeTrialCandidateCount = (activeTrialsPartitioned?.primary.length ?? 0) + (activeTrialsPartitioned?.related.length ?? 0);
436
- // Map ctgov active trials to clinical-trial row shape with text-based confidence,
437
- // then remove rows identified as off-target by the attribution filter.
438
- // Drug-in-class matches (calcitonin drugs for CALCR, GLP-1 drugs for GLP1R, etc.)
439
- // are promoted to "medium" via classifyClinicalTrialConfidence so they survive
440
- // the ≥ medium threshold even when the gene symbol is absent from intervention names.
441
- const clinicalTrialRows = activeTrialsPartitioned !== null
442
- ? activeTrialsPartitioned.primary
443
- .map((t) => {
444
- const text_confidence = classifyClinicalTrialConfidence(t, resolved.geneSymbol, knownClassDrugNames);
445
- return {
446
- nct_id: t.nctId,
447
- title: t.title,
448
- phase: t.phase,
449
- status: t.status,
450
- conditions: t.conditions,
451
- start_date: t.startDate,
452
- completion_date: t.primaryCompletionDate,
453
- match_confidence: text_confidence,
454
- };
455
- })
456
- .filter((r) => r.match_confidence !== "low")
457
- : null;
458
- // Build failed-trial rows with off-target partitioning. The gene-symbol
459
- // query in collectCtgov misses terminated trials whose intervention is
460
- // recorded only by sponsor code or brand name (e.g., SMC021 for oral
461
- // salmon calcitonin), so we supplement with a drug-name query against
462
- // CT.gov keyed by the assembled drug interactions. The function self-
463
- // throttles via withHost("ctgov", ...) on its per-drug call, so the
464
- // assembler does not need its own outer wrapper. Per-drug failures
465
- // inside searchFailedTrialsForDrugNames are isolated.
466
- const drugNameFailedTrials = drugInteractionRows && drugInteractionRows.length > 0
467
- ? await searchFailedTrialsForDrugNames(drugInteractionRows.map((d) => d.drug_name).filter(Boolean))
468
- .then((r) => r.trials)
469
- .catch(() => [])
470
- : [];
471
- const failedById = new Map();
472
- if (ctgov.coverage === "available") {
473
- for (const t of ctgov.data.failed) {
474
- failedById.set(t.nctId, {
475
- nctId: t.nctId,
476
- title: t.title,
477
- interventions: t.interventions,
478
- conditions: t.conditions,
479
- });
480
- }
481
- }
482
- for (const t of drugNameFailedTrials) {
483
- if (!failedById.has(t.nctId)) {
484
- failedById.set(t.nctId, {
485
- nctId: t.nctId,
486
- title: t.title,
487
- interventions: t.interventions,
488
- conditions: t.conditions,
489
- });
490
- }
491
- }
492
- const rawFailedTrials = [...failedById.values()].map((t) => {
493
- const fanoutItem = fanout?.perTrialAes.results.find((r) => r.coverage === "available" && r.data.nctId === t.nctId);
494
- const whyStopped = fanoutItem?.coverage === "available" ? (fanoutItem.data.whyStopped ?? "") : "";
495
- return {
496
- nctId: t.nctId,
497
- title: t.title,
498
- interventions: t.interventions,
499
- conditions: t.conditions,
500
- why_stopped: whyStopped,
501
- failure_category: buildFailureCategoryDiscriminated(whyStopped),
502
- classifier: "rules",
503
- };
504
- });
505
- const failedTrialsPartitioned = rawFailedTrials.length > 0 ? await partitionTrialsByAttribution(rawFailedTrials, attrCtx) : null;
506
- const failedTrialRows = failedTrialsPartitioned?.primary.map((t) => ({
507
- nct_id: t.nctId,
508
- title: t.title,
509
- why_stopped: t.why_stopped,
510
- failure_category: t.failure_category,
511
- classifier: t.classifier,
512
- })) ?? [];
513
- const failedRelatedRows = failedTrialsPartitioned?.related.map((t) => ({
514
- nct_id: t.nctId,
515
- title: t.title,
516
- why_stopped: t.why_stopped,
517
- failure_category: t.failure_category,
518
- classifier: t.classifier,
519
- })) ?? [];
520
- const attributedFailedRows = failedTrialRows.map(attachTrialAttribution);
521
- const attributedFailedRelatedRows = failedRelatedRows.map(attachTrialAttribution);
522
- // The same partition the clinical-development section reports, shared with the
523
- // safety lens: a related-receptor failure is context for both, and it must be
524
- // told apart from a target-attributed one in both.
525
- const eligibleFailedRows = attributedFailedRows.filter((r) => r.eligible_for_toxicology_aggregation);
526
- const excludedFailedRows = [...attributedFailedRows.filter((r) => !r.eligible_for_toxicology_aggregation), ...attributedFailedRelatedRows];
527
- // `clinical_development.failed_trials` and `safety_profile.failed_trials_safety_lens`
528
- // report the same partition under two headings, so the section is decided once.
529
- // Two constructions of it would let the same condition report two coverages.
530
- const failedTrialsSection = ctgov.coverage === "available"
531
- ? coverageFromFilteredRows({
532
- data: {
533
- rows: eligibleFailedRows,
534
- excluded_rows: excludedFailedRows,
535
- },
536
- retainedCount: eligibleFailedRows.length,
537
- droppedCount: excludedFailedRows.length,
538
- filter: FAILED_TRIALS_FILTER,
539
- emptyReason: "ClinicalTrials.gov returned no terminated or withdrawn trials for the assessment target",
540
- })
541
- : { coverage: "queried_no_data", error: { message: "ClinicalTrials.gov unavailable" } };
542
- // Typed explicitly so literal string coverage values narrow correctly.
543
- // Phase-5 persist attaches the derived sub-tree after synthesis is stamped.
544
- const dossierBody = {
545
- generated_at: new Date().toISOString(),
546
- entity: assembleEntity(resolved),
547
- liability_summary: liabilitySummary,
548
- tractability: tractabilitySection,
549
- indications: ot.coverage === "available"
550
- ? coverageFromFilteredRows({
551
- data: {
552
- rows: indicationRows,
553
- excluded_unsupported_count: unsupportedCandidates.length,
554
- ...(unsupportedCandidates.length > 0
555
- ? {
556
- unsupported_associations: unsupportedCandidates.map(({ a }) => ({
557
- disease_id: a.diseaseId,
558
- disease_name: a.diseaseName,
559
- })),
560
- }
561
- : {}),
562
- },
563
- retainedCount: indicationRows.length,
564
- // The head cap is a carrying limit rather than a judgement, so
565
- // only the two content filters count against completeness.
566
- droppedCount: indicationsRaw.length - filteredIndications.length + unsupportedCandidates.length,
567
- filter: INDICATIONS_FILTER,
568
- emptyReason: "Open Targets returned no disease associations for the target",
569
- })
570
- : {
571
- coverage: "queried_no_data",
572
- error: {
573
- message: ot.coverage === "queried_no_data" && ot.error?.message ? ot.error.message : "Open Targets unavailable",
574
- },
575
- },
576
- drug_interactions: drugInteractionRows
577
- ? { coverage: "available", data: { rows: drugInteractionRows } }
578
- : { coverage: "queried_no_data", error: { message: "no ChEMBL modulators available" } },
579
- clinical_development: {
580
- trials: clinicalTrialRows === null
581
- ? { coverage: "queried_no_data", error: { message: "ClinicalTrials.gov unavailable" } }
582
- : (() => {
583
- const attributed = clinicalTrialRows.map(attachTrialAttribution);
584
- const relatedTargetTrials = (activeTrialsPartitioned?.related ?? []).map((t) => attachTrialAttribution({
585
- nct_id: t.nctId,
586
- title: t.title,
587
- phase: t.phase,
588
- status: t.status,
589
- conditions: t.conditions,
590
- start_date: t.startDate,
591
- completion_date: t.primaryCompletionDate,
592
- match_confidence: "off_target",
593
- }));
594
- const rows = attributed.filter((r) => r.eligible_for_toxicology_aggregation);
595
- return coverageFromFilteredRows({
596
- data: {
597
- rows,
598
- excluded_rows: [...attributed.filter((r) => !r.eligible_for_toxicology_aggregation), ...relatedTargetTrials],
599
- selection_criteria: {
600
- derived_from: "analytics.discovery_trials",
601
- min_confidence: "medium",
602
- excluded_off_target_count: activeTrialsPartitioned?.related.length ?? 0,
603
- },
604
- },
605
- retainedCount: rows.length,
606
- // Everything CT.gov returned for the target, against what survived:
607
- // the low-confidence cut, the attribution cut, and the related-receptor
608
- // partition are all filters of ours.
609
- droppedCount: activeTrialCandidateCount - rows.length,
610
- filter: ACTIVE_TRIALS_FILTER,
611
- emptyReason: "ClinicalTrials.gov returned no trials for the assessment target",
612
- });
613
- })(),
614
- outcomes: trialOutcomesRows
615
- ? { coverage: "available", data: { rows: trialOutcomesRows.map(attachTrialAttribution) } }
616
- : phase3
617
- ? { coverage: "queried_no_data", error: { message: "no per-trial outcomes available" } }
618
- : { coverage: "not_loaded", reason: "Phase-3 fan-out not run" },
619
- failed_trials: failedTrialsSection,
620
- benchmarks: {
621
- therapeutic_area: benchmarks.therapeutic_area,
622
- fallback_to_all_areas: benchmarks.source === "fallback",
623
- phase_transitions: benchmarks.transitions,
624
- likelihood_of_approval: benchmarks.transitions.phase1_to_approval,
625
- source_attribution: `${datasetAttr.source} (${datasetAttr.data_version}, ${datasetAttr.data_window})`,
626
- },
627
- },
628
- safety_profile: {
629
- organ_rollup: organRollupRows
630
- ? { coverage: "available", data: { rows: organRollupRows } }
631
- : phase3
632
- ? { coverage: "queried_no_data", error: { message: "no organ-level signals" } }
633
- : { coverage: "not_loaded", reason: "Phase-3 fan-out not run" },
634
- faers: (() => {
635
- const classPrecedentSection = classP
636
- ? { coverage: "available", data: classP }
637
- : phase3
638
- ? { coverage: "queried_no_data", error: { message: "no class precedent data" } }
639
- : { coverage: "not_loaded", reason: "Phase-3 fan-out not run" };
640
- const rawFaers = faersAgg
641
- ? { coverage: "available", data: faersAgg }
642
- : phase3
643
- ? { coverage: "queried_no_data", error: { message: "FAERS returned no reports" } }
644
- : { coverage: "not_loaded", reason: "Phase-3 fan-out not run" };
645
- return reconcileFaersCoverage({ faers: rawFaers, class_precedent: classPrecedentSection });
646
- })(),
647
- trial_aes: trialAesAgg
648
- ? { coverage: "available", data: trialAesAgg }
649
- : phase3
650
- ? { coverage: "queried_no_data", error: { message: "no trial AE data" } }
651
- : { coverage: "not_loaded", reason: "Phase-3 fan-out not run" },
652
- off_target_panel: offTargetPanel
653
- ? { coverage: "available", data: offTargetPanel }
654
- : phase3
655
- ? { coverage: "queried_no_data", error: { message: "no off-target data" } }
656
- : { coverage: "not_loaded", reason: "Phase-3 fan-out not run" },
657
- // The safety lens carries the same two buckets as
658
- // `clinical_development.failed_trials`. Merging them would put a
659
- // related-receptor termination in the same list as a target-attributed
660
- // one, and the reader has no other way to tell them apart.
661
- failed_trials_safety_lens: failedTrialsSection,
662
- class_precedent: classP
663
- ? { coverage: "available", data: classP }
664
- : phase3
665
- ? { coverage: "queried_no_data", error: { message: "no class precedent data" } }
666
- : { coverage: "not_loaded", reason: "Phase-3 fan-out not run" },
667
- // Stamped by Phase 5 after the safety-flags-trail agent runs.
668
- target_organ_liabilities: { coverage: "not_loaded", reason: NOT_LOADED_PHASE5 },
669
- regulatory_actions: regulatoryActionRows.length > 0
670
- ? { coverage: "available", data: { rows: regulatoryActionRows } }
671
- : { coverage: "queried_no_data" },
672
- },
673
- // The corroboration fold reads the FDA label signals segmented after
674
- // Phase 4, and the claim investigation reads the fold, so both are
675
- // stamped by the workflow body before synthesis runs.
676
- safety_corroboration: { coverage: "not_loaded", reason: NOT_LOADED_POST_ASSEMBLY },
677
- claim_investigation: { coverage: "not_loaded", reason: "the claim investigation runs after Phase 4, over the corroborated organ claims" },
678
- off_tissue_risk: offTissueSection,
679
- off_target_panel: offTargetPanel
680
- ? { coverage: "available", data: offTargetPanel }
681
- : phase3
682
- ? { coverage: "queried_no_data", error: { message: "no off-target data" } }
683
- : { coverage: "not_loaded", reason: "Phase-3 fan-out not run" },
684
- reference_biology: {
685
- therapeutic_area_associations: ot.coverage === "available"
686
- ? coverageFromFilteredRows({
687
- data: {
688
- rows: filteredIndications.slice(0, 200).map((a) => {
689
- const evidence = makeAssociationEvidence(a);
690
- return {
691
- partner_id: a.diseaseId,
692
- partner_name: a.diseaseName,
693
- predicate: a.geneticAssociationScore != null
694
- ? "associated_with"
695
- : a.knownDrugScore != null
696
- ? "therapeutic_target"
697
- : a.animalModelScore != null
698
- ? "validated_in_animal_model"
699
- : "literature_co_occurrence",
700
- best_score: a.score,
701
- source_count: [
702
- a.geneticAssociationScore,
703
- a.knownDrugScore,
704
- a.animalModelScore,
705
- a.somaticMutationScore,
706
- a.literatureScore,
707
- ].filter((s) => s != null).length,
708
- paper_count: a.literaturePmids?.length ?? 0,
709
- evidence,
710
- };
711
- }),
712
- },
713
- retainedCount: Math.min(filteredIndications.length, 200),
714
- droppedCount: indicationsRaw.length - filteredIndications.length,
715
- filter: INDICATIONS_FILTER,
716
- emptyReason: "Open Targets returned no disease associations for the target",
717
- })
718
- : { coverage: "queried_no_data" },
719
- molecular_interactions: (() => {
720
- const rows = assembleMolecularInteractions(phase2, fanout);
721
- return rows ? { coverage: "available", data: { rows } } : { coverage: "queried_no_data" };
722
- })(),
723
- biomarker_potential: (() => {
724
- const rows = assembleBiomarkerPotential(phase2);
725
- return rows ? { coverage: "available", data: { rows } } : { coverage: "queried_no_data" };
726
- })(),
727
- genetic_alterations: {
728
- somatic: cbioportal.coverage === "available"
729
- ? coverageFromRows(cbioportal.data.rows.map((r) => ({
730
- cancer_type: r.cancerTypeName,
731
- cohort: r.studies.join(", "),
732
- mutation_count: r.mutatedSamples,
733
- total_samples: r.totalSamples,
734
- frequency: r.frequency,
735
- source: "cbioportal",
736
- })), { reason: "cBioPortal returned no somatic mutation cohorts for this gene" })
737
- : { coverage: "queried_no_data" },
738
- clinvar: clinvar.coverage === "available"
739
- ? coverageFromRows(clinvar.data.variants.map((v) => ({
740
- variant_id: v.variationId,
741
- hgvs: v.title,
742
- classification: v.clinicalSignificance,
743
- condition: v.conditions[0] ?? "",
744
- review_status: v.reviewStatus,
745
- })), { reason: "ClinVar returned 0 variants for this gene" })
746
- : { coverage: "queried_no_data" },
747
- },
748
- resistance_evidence: (() => {
749
- const rows = assembleResistanceEvidence(phase2);
750
- return rows ? { coverage: "available", data: { rows } } : { coverage: "queried_no_data" };
751
- })(),
752
- combination_evidence: (() => {
753
- const rows = assembleCombinationEvidence(phase2);
754
- return rows ? { coverage: "available", data: { rows } } : { coverage: "queried_no_data" };
755
- })(),
756
- pathway_context: pathways.coverage === "available"
757
- ? (() => {
758
- const presentDbs = new Set(pathways.data.pathways.map((p) => p.source));
759
- const ALL_DBS = ["reactome", "kegg", "wikipathways", "msigdb"];
760
- const QUERIED = ["reactome", "kegg"];
761
- return {
762
- coverage: "available",
763
- data: {
764
- rows: pathways.data.pathways.map((p) => ({
765
- pathway_id: p.id,
766
- pathway_name: p.name,
767
- database: p.source,
768
- evidence_score: 1,
769
- })),
770
- databases_queried: QUERIED.filter((db) => presentDbs.has(db)).length === QUERIED.length
771
- ? QUERIED
772
- : QUERIED.filter((db) => presentDbs.has(db)),
773
- databases_skipped: ALL_DBS.filter((db) => !presentDbs.has(db)),
774
- },
775
- };
776
- })()
777
- : { coverage: "queried_no_data" },
778
- ppi_network: ppi.coverage === "available"
779
- ? {
780
- coverage: "available",
781
- data: { partners: dedupePpiPartners(ppi.data.partners) },
782
- }
783
- : { coverage: "queried_no_data" },
784
- normal_tissue_expression: expression.coverage === "available"
785
- ? {
786
- coverage: "available",
787
- data: {
788
- source: expression.data.source,
789
- unit: expression.data.unit,
790
- normalization_notes: expression.data.normalization_notes,
791
- rows: expression.data.tissues
792
- .filter((t) => t.value != null)
793
- .sort((a, b) => (b.value ?? 0) - (a.value ?? 0))
794
- .map((t) => ({
795
- tissue: t.tissueLabel,
796
- value: t.value ?? 0,
797
- })),
798
- },
799
- }
800
- : { coverage: "queried_no_data" },
801
- preclinical: {
802
- ko_phenotype: impc.coverage === "available"
803
- ? {
804
- coverage: "available",
805
- data: {
806
- marker_symbol: impc.data.mouseMarkerSymbol,
807
- viability: impc.data.viability,
808
- sex_dimorphism: impc.data.sexDimorphic,
809
- organ_systems_with_phenotype: toOrganSystems(impc.data.organSystems),
810
- top_mp_terms: impc.data.mpTerms.slice(0, 25).map((m) => m.term),
811
- total_phenotype_count: impc.data.phenotypeCount,
812
- pre_weaning_lethal: impc.data.viability === "lethal_pre_weaning",
813
- supporting_literature: koSupportingLiterature,
814
- },
815
- }
816
- : { coverage: "queried_no_data" },
817
- expression_heatmap: expressionMulti.coverage === "available"
818
- ? {
819
- coverage: "available",
820
- data: {
821
- cells: expressionMulti.data.bySpecies.flatMap((s) => {
822
- const speciesKey = s.species === "homo_sapiens"
823
- ? "human"
824
- : s.species === "mus_musculus"
825
- ? "mouse"
826
- : s.species === "rattus_norvegicus"
827
- ? "rat"
828
- : s.species === "macaca_mulatta"
829
- ? "macaque"
830
- : "dog";
831
- return s.tissues.map((t) => ({
832
- tissue: t.tissue,
833
- species: speciesKey,
834
- rank: t.rank,
835
- }));
836
- }),
837
- per_species_coverage: Object.fromEntries(expressionMulti.data.bySpecies.map((s) => [
838
- s.species,
839
- s.tissues.length > 0 ? "available" : "queried_no_data",
840
- ])),
841
- },
842
- }
843
- : { coverage: "queried_no_data" },
844
- translational_commentary: {
845
- coverage: "not_loaded",
846
- reason: NOT_LOADED_PHASE5,
847
- },
848
- preclinical_literature: preclinicalLitData
849
- ? { coverage: "available", data: preclinicalLitData }
850
- : { coverage: "queried_no_data", error: { message: "no species-tagged literature" } },
851
- data_coverage: {
852
- ko: impc.coverage === "available" ? "available" : "queried_no_data",
853
- expression: expressionMulti.coverage,
854
- literature: pubmed.coverage,
855
- per_species: expressionMulti.coverage === "available"
856
- ? Object.fromEntries(expressionMulti.data.bySpecies.map((s) => [s.species, s.tissues.length > 0]))
857
- : {},
858
- },
859
- },
860
- key_papers: keyPapersRows
861
- ? { coverage: "available", data: { rows: keyPapersRows } }
862
- : { coverage: "queried_no_data", error: { message: "no PubMed results" } },
863
- },
864
- analytics: {
865
- evidence_conflicts: (() => {
866
- const rows = assembleEvidenceConflicts(phase2);
867
- return rows ? { coverage: "available", data: { rows } } : { coverage: "queried_no_data" };
868
- })(),
869
- evidence_timeline: evidenceTimeline
870
- ? { coverage: "available", data: evidenceTimeline }
871
- : pubmed.coverage === "not_loaded"
872
- ? { coverage: "not_loaded", reason: "PubMed not queried" }
873
- : { coverage: "queried_no_data", error: { message: "no PubMed years available" } },
874
- translational_chain: translationalChain
875
- ? { coverage: "available", data: translationalChain }
876
- : pubmed.coverage === "not_loaded" && ctgov.coverage === "not_loaded" && impc.coverage === "not_loaded"
877
- ? { coverage: "not_loaded", reason: "no upstream tier evidence" }
878
- : { coverage: "queried_no_data", error: { message: "no tier evidence" } },
879
- additional_evidence: (() => {
880
- const data = assembleAdditionalEvidence(phase2);
881
- return data ? { coverage: "available", data } : { coverage: "queried_no_data" };
882
- })(),
883
- discovery_trials: discoveryTrials,
884
- // Stamped by Phase 5 after synthesis agents run; not available at assembly time.
885
- synthesis_diagnostics: { coverage: "not_loaded", reason: "Phase-5 synthesis not yet run" },
886
- quality_gates: { coverage: "not_loaded", reason: "Phase-5 synthesis not yet run" },
887
- },
888
- // Stamped by Phase 5 after the dossier-recommendation agent runs.
889
- executive_recommendation: { coverage: "not_loaded", reason: "Phase-5 synthesis not yet run" },
890
- };
891
- return dossierBody;
892
- }
893
- /**
894
- * Reconciles target-level FAERS with class_precedent aggregates.
895
- *
896
- * When target-level FAERS has data (coverage: "available"), pass through
897
- * unchanged. When class_precedent has FAERS-derived reports in per_organ,
898
- * fold them into a class-scoped FAERS summary payload with
899
- * coverage: "available" and inference_path tagged — rather than leaving
900
- * the section as queried_no_data, which creates an evidence conflict
901
- * that the conflict-detector flags. When neither has data, return
902
- * faers unchanged.
903
- */
904
- export function reconcileFaersCoverage(input) {
905
- const { faers, class_precedent: classPrecedent } = input;
906
- // Target-level FAERS already has data — identity pass-through.
907
- if (faers.coverage === "available")
908
- return faers;
909
- if (!classPrecedent || classPrecedent.coverage === "queried_no_data" || classPrecedent.coverage === "not_loaded")
910
- return faers;
911
- const perOrgan = classPrecedent.data?.per_organ ?? [];
912
- const aes = perOrgan.flatMap((po) => (po.top_aes ?? []).map((ae) => ({
913
- meddra_term: ae.term ?? "",
914
- organ: po.organ,
915
- report_count: ae.report_count ?? 0,
916
- })));
917
- // No class-level AEs either — leave the section as-is.
918
- if (aes.length === 0)
919
- return faers;
920
- // Each AE-term count in top_aes counts reports that have that specific AE.
921
- // A report with multiple AEs (e.g. NAUSEA + DIZZINESS) contributes to each
922
- // term's count, so this sum is an upper bound on unique reports, not the
923
- // true unique count. The class_precedent structure does not preserve unique
924
- // report counts — this is the best approximation available.
925
- const total = aes.reduce((s, ae) => s + ae.report_count, 0);
926
- return {
927
- coverage: "available",
928
- data: {
929
- total_reports: total,
930
- seriousness: { coverage: "queried_no_data", total_reports: total },
931
- top_signals: aes
932
- .slice()
933
- .sort((a, b) => b.report_count - a.report_count)
934
- .slice(0, 20),
935
- // class_precedent.per_organ aggregates across drugs without preserving
936
- // per-drug report counts. Emitting per_modulator entries with
937
- // report_count: 0 would look like real measured zeros to any consumer
938
- // summing them to cross-check total_reports. Returning an empty array
939
- // is the honest representation — the per-drug breakdown is not known
940
- // at this layer.
941
- per_modulator: [],
942
- },
943
- inference_path: "class_precedent.per_organ → FAERS",
944
- };
945
- }