@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,971 +0,0 @@
1
- import { coverageFromFilteredRows } from "../coverage.js";
2
- import { classifyTrialAttribution } from "../lib/target-identity-filter.js";
3
- import { HIGH_EXPRESSION_TPM_THRESHOLD } from "../lib/expression-constants.js";
4
- export { HIGH_EXPRESSION_TPM_THRESHOLD };
5
- // ── Shared helpers (pulled up so extracted functions can reference them) ──
6
- const SAFETY_RELEVANT_ORGANS = new Set([
7
- "heart",
8
- "liver",
9
- "kidney",
10
- "brain",
11
- "bone marrow",
12
- "lung",
13
- "thyroid",
14
- "pancreas",
15
- "adrenal",
16
- "gonad",
17
- "ovary",
18
- "testis",
19
- "stomach",
20
- "intestine",
21
- "spleen",
22
- "lymph node",
23
- "retina",
24
- "cochlea",
25
- // Reproductive — ovary/testis/gonad already above; add remaining reproductive tissues
26
- "placenta",
27
- "uterus",
28
- "endometrium",
29
- "fallopian tube",
30
- "epididymis",
31
- "prostate",
32
- "seminal vesicle",
33
- // Bone — bone marrow already above; add cortical/trabecular bone and named bones
34
- "bone",
35
- "tibia",
36
- "femur",
37
- "vertebra",
38
- ]);
39
- function isSafetyRelevant(tissue, organ) {
40
- const t = tissue.toLowerCase();
41
- const o = (organ ?? "").toLowerCase();
42
- for (const safe of SAFETY_RELEVANT_ORGANS) {
43
- if (t.includes(safe) || o.includes(safe))
44
- return true;
45
- }
46
- return false;
47
- }
48
- function makeAssociationEvidence(a) {
49
- const out = [];
50
- if (a.geneticAssociationScore != null) {
51
- out.push({
52
- source: "opentargets:genetic_association",
53
- score: a.geneticAssociationScore,
54
- predicate: "associated_with",
55
- is_human: true,
56
- metadata: { disease_id: a.diseaseId },
57
- });
58
- }
59
- if (a.knownDrugScore != null) {
60
- out.push({
61
- source: "opentargets:known_drug",
62
- score: a.knownDrugScore,
63
- predicate: "therapeutic_target",
64
- is_human: true,
65
- is_clinical: true,
66
- metadata: { disease_id: a.diseaseId },
67
- });
68
- }
69
- if (a.animalModelScore != null) {
70
- out.push({
71
- source: "opentargets:animal_model",
72
- score: a.animalModelScore,
73
- predicate: "validated_in_animal_model",
74
- metadata: { disease_id: a.diseaseId },
75
- });
76
- }
77
- if (a.somaticMutationScore != null) {
78
- out.push({
79
- source: "opentargets:somatic_mutation",
80
- score: a.somaticMutationScore,
81
- predicate: "somatic_mutation_associated",
82
- is_human: true,
83
- metadata: { disease_id: a.diseaseId },
84
- });
85
- }
86
- if (a.literatureScore != null) {
87
- out.push({
88
- source: "opentargets:literature",
89
- score: a.literatureScore,
90
- predicate: "literature_co_occurrence",
91
- metadata: { disease_id: a.diseaseId },
92
- });
93
- }
94
- return out;
95
- }
96
- // ── Extracted content (lines 1544–2504, 2541–2678 from index.ts) ──────
97
- const SPECIES_PATTERNS = [
98
- { species: "mouse", rx: /\b(mouse|mice|murine)\b/i },
99
- { species: "rat", rx: /\brats?\b/i },
100
- { species: "macaque", rx: /\b(macaque|cynomolgus|rhesus)\b/i },
101
- { species: "dog", rx: /\b(canine|beagle)\b/i },
102
- { species: "human", rx: /\b(human|patient|cohort)\b/i },
103
- ];
104
- const IN_VITRO_RX = /\b(organoid|spheroid|cell line|cell-line|primary cells?|cultured|culture|in vitro|cell culture|hipsc|ipsc|3d culture|microfluidic|chip|on a chip)\b/i;
105
- function inferSpeciesFromTitle(title) {
106
- let species = null;
107
- for (const p of SPECIES_PATTERNS) {
108
- if (p.rx.test(title)) {
109
- species = p.species;
110
- break;
111
- }
112
- }
113
- if (!species)
114
- return null;
115
- if (IN_VITRO_RX.test(title)) {
116
- return { species, modelSystem: "complex_in_vitro" };
117
- }
118
- return {
119
- species,
120
- modelSystem: species === "human" ? "ex_vivo_human" : "in_vivo_animal",
121
- };
122
- }
123
- export function assemblePreclinicalLiterature(phase2) {
124
- const pubmed = phase2.phase1.collectors.pubmedIndex;
125
- if (pubmed.coverage !== "available")
126
- return null;
127
- const all = pubmed.data.results;
128
- const speciesRows = all
129
- .map((r) => {
130
- const inf = inferSpeciesFromTitle(r.title);
131
- if (!inf || inf.species === "human")
132
- return null;
133
- return {
134
- pmid: r.pmid,
135
- claim: r.title,
136
- excerpt: undefined,
137
- model_system: inf.modelSystem,
138
- species: inf.species,
139
- };
140
- })
141
- .filter((r) => r !== null);
142
- const truncated = speciesRows.length > 50;
143
- const rows = speciesRows.slice(0, 50);
144
- if (rows.length === 0)
145
- return null;
146
- return {
147
- rows,
148
- total_claim_count: speciesRows.length,
149
- truncated,
150
- };
151
- }
152
- export function assembleKeyPapers(phase2, internalRefCounts) {
153
- const pubmed = phase2.phase1.collectors.pubmedIndex;
154
- if (pubmed.coverage !== "available" || pubmed.data.results.length === 0) {
155
- return null;
156
- }
157
- const rows = pubmed.data.results.map((r) => {
158
- const yearNum = parseInt(r.year, 10);
159
- return {
160
- pmid: r.pmid,
161
- title: r.title,
162
- internal_reference_count: internalRefCounts.get(r.pmid) ?? 0,
163
- year: Number.isFinite(yearNum) ? yearNum : undefined,
164
- };
165
- });
166
- rows.sort((a, b) => b.internal_reference_count - a.internal_reference_count);
167
- return rows.slice(0, 25);
168
- }
169
- export function tallyInternalReferences(evidenceArrays) {
170
- const counts = new Map();
171
- for (const arr of evidenceArrays) {
172
- for (const e of arr) {
173
- if (e.pmid)
174
- counts.set(e.pmid, (counts.get(e.pmid) ?? 0) + 1);
175
- }
176
- }
177
- return counts;
178
- }
179
- // Loss-of-function literature signal — surface forms used in PubMed
180
- // titles to describe knockout, knockdown, null/deficient mice, conditional
181
- // alleles, CRISPR-mediated ablation, and homozygous-null notation. A title
182
- // matches if any one pattern hits.
183
- const KO_LITERATURE_PATTERNS = [
184
- /\bknock[- ]?outs?\b/i,
185
- // Bare "knockdown" removed — too broad; siRNA/shRNA-mediated knockdown is
186
- // covered by the specific pattern below. Keeping it here admitted siRNA
187
- // cancer-cell-line papers that are not germline KO evidence.
188
- /\bconditional (?:ko|knock[- ]?out|deletion|ablation|allele)s?\b/i,
189
- /\b(?:gene|genetic|germline|somatic) (?:deletion|ablation|disruption|inactivation)\b/i,
190
- /\bnull (?:mice|mouse|allele|mutants?)\b/i,
191
- /\b[A-Za-z0-9]+[- ](?:deficient|null) (?:mice|mouse)\b/i,
192
- /\b(?:cre[/-]?lox(?:p)?|floxed|fl\/fl)\b/i,
193
- /\bcrispr(?:[/-]cas9?)?[- ]?(?:knock[- ]?out|deletion|ablation|mediated|edited)\b/i,
194
- /\b(?:loss[- ]of[- ]function|haploinsufficien(?:cy|t))\b/i,
195
- /\b(?:shrna|sirna|antisense oligonucleotide)[- ](?:mediated )?(?:knock[- ]?down|silencing|depletion)\b/i,
196
- /\bKO\b/, // case-sensitive: the bio abbreviation, not "ko" inside ordinary words
197
- /[-‐–−]\/[-‐–−]/, // -/- or unicode-dash homozygous-null notation
198
- ];
199
- // Patterns that disqualify a title despite matching the inclusion list above.
200
- //
201
- // Bare "knockdown" and RNAi reagents (siRNA/shRNA) are excluded because they
202
- // describe transient cell-line experiments, not germline or conditional KO models.
203
- // Somatic LOF patterns (mutations/LOF in the context of cancer/tumour pathology)
204
- // are excluded because they describe tumour-sequencing findings, not animal models.
205
- // "/\bcancer\b/" and "/\btumou?r\b/" are intentionally NOT in this list: a
206
- // legitimate paper such as "Tp53 KO mice are tumor-prone" mentions the tumour
207
- // phenotype of the KO model and should remain included. We rely on the somatic
208
- // context patterns (somatic+mutation, LOF+cancer) to catch sequencing papers.
209
- const KO_EXCLUDE_PATTERNS = [
210
- /\bsi[- ]?rna\b/i,
211
- /\bsh[- ]?rna\b/i,
212
- /\bknockdown\b/i,
213
- /\bsomatic\b.*\b(?:mutation|variant|loss[- ]of[- ]function)\b/i,
214
- /\bloss[- ]of[- ]function\b.*\b(?:cancer|tumou?r|glioblastoma|carcinoma|sarcoma|lymphoma|leukemia|melanoma)\b/i,
215
- /\b(?:cancer|tumou?r|glioblastoma|carcinoma|sarcoma|lymphoma|leukemia|melanoma)\b.*\bloss[- ]of[- ]function\b/i,
216
- ];
217
- export function looksLikeKoLiterature(title) {
218
- if (KO_EXCLUDE_PATTERNS.some((re) => re.test(title)))
219
- return false;
220
- return KO_LITERATURE_PATTERNS.some((rx) => rx.test(title));
221
- }
222
- export function assembleKoSupportingLiterature(phase2) {
223
- const pubmed = phase2.phase1.collectors.pubmedIndex;
224
- if (pubmed.coverage !== "available")
225
- return [];
226
- return pubmed.data.results
227
- .filter((r) => looksLikeKoLiterature(r.title))
228
- .slice(0, 10)
229
- .map((r) => ({
230
- source: "pubmed",
231
- predicate: "ko_phenotype",
232
- pmid: r.pmid,
233
- excerpt: r.title,
234
- metadata: { year: r.year, journal: r.journal },
235
- }));
236
- }
237
- // ── Indications composite + filters ─────────────────────────────────
238
- const MEASUREMENT_TERMS = /\b(level|levels|concentration|count|measurement|content|index|score)\s*$/i;
239
- export function assembleMolecularInteractions(phase2, fanout) {
240
- const ppi = phase2.phase1.collectors.stringPpi;
241
- const polypharm = fanout?.perModulatorPolypharm.results ?? [];
242
- const rows = [];
243
- if (ppi.coverage === "available") {
244
- const partners = dedupePpiPartners(ppi.data.partners);
245
- for (const p of partners.slice(0, 50)) {
246
- rows.push({
247
- partner_id: p.partner_id,
248
- partner_name: p.partner_name,
249
- predicate: "binding",
250
- best_score: p.combined_score,
251
- source_count: p.sources.length,
252
- paper_count: 0,
253
- evidence: [
254
- {
255
- source: "string",
256
- predicate: "binding",
257
- score: p.string_score ?? 0,
258
- metadata: { combined_score: p.combined_score },
259
- },
260
- ],
261
- });
262
- }
263
- }
264
- const byTarget = new Map();
265
- for (const item of polypharm) {
266
- if (item.coverage !== "available")
267
- continue;
268
- for (const hit of item.data.hits) {
269
- const cur = byTarget.get(hit.targetChemblId);
270
- const pch = hit.pchemblValue ?? 0;
271
- if (!cur || pch > cur.pchembl) {
272
- byTarget.set(hit.targetChemblId, {
273
- name: hit.targetName ?? hit.targetChemblId,
274
- pchembl: pch,
275
- modulators: cur?.modulators ?? new Set(),
276
- });
277
- }
278
- byTarget.get(hit.targetChemblId).modulators.add(item.data.moleculeChemblId);
279
- }
280
- }
281
- for (const [id, v] of byTarget) {
282
- rows.push({
283
- partner_id: id,
284
- partner_name: v.name ?? id,
285
- predicate: "drug_binding",
286
- best_score: v.pchembl,
287
- source_count: 1,
288
- paper_count: 0,
289
- evidence: [...v.modulators].map((m) => ({
290
- source: "chembl:polypharm",
291
- predicate: "drug_binding",
292
- score: v.pchembl,
293
- metadata: { partner_id: id, modulator: m },
294
- })),
295
- });
296
- }
297
- if (rows.length === 0)
298
- return null;
299
- return rows.slice(0, 100);
300
- }
301
- export function assembleBiomarkerPotential(phase2) {
302
- const ot = phase2.phase1.collectors.opentargets;
303
- const clinvar = phase2.phase1.collectors.clinvar;
304
- const rows = [];
305
- if (ot.coverage === "available") {
306
- for (const a of ot.data.associations) {
307
- if (a.knownDrugScore == null || a.knownDrugScore < 0.5)
308
- continue;
309
- rows.push({
310
- partner_id: a.diseaseId,
311
- partner_name: a.diseaseName,
312
- predicate: "predictive",
313
- best_score: a.knownDrugScore,
314
- source_count: 1,
315
- paper_count: 0,
316
- evidence: [
317
- {
318
- source: "opentargets:known_drug",
319
- predicate: "predictive",
320
- score: a.knownDrugScore,
321
- metadata: { disease_id: a.diseaseId },
322
- },
323
- ],
324
- metrics: {},
325
- });
326
- }
327
- }
328
- if (clinvar.coverage === "available") {
329
- const pathogenic = clinvar.data.variants.filter((v) => /pathogenic/i.test(v.clinicalSignificance));
330
- if (pathogenic.length > 0) {
331
- const conditions = new Map();
332
- for (const v of pathogenic) {
333
- for (const c of v.conditions) {
334
- if (!c)
335
- continue;
336
- conditions.set(c, (conditions.get(c) ?? 0) + 1);
337
- }
338
- }
339
- for (const [condition, count] of conditions) {
340
- rows.push({
341
- partner_id: null,
342
- partner_name: condition,
343
- predicate: "predictive",
344
- best_score: Math.min(1, count / 10),
345
- source_count: 1,
346
- paper_count: 0,
347
- evidence: [
348
- {
349
- source: "clinvar",
350
- predicate: "predictive",
351
- score: Math.min(1, count / 10),
352
- metadata: { variant_count: count },
353
- },
354
- ],
355
- metrics: {},
356
- });
357
- }
358
- }
359
- }
360
- rows.sort((a, b) => b.best_score - a.best_score);
361
- if (rows.length === 0)
362
- return null;
363
- return rows.slice(0, 50);
364
- }
365
- export function assembleResistanceEvidence(phase2) {
366
- const pubmed = phase2.phase1.collectors.pubmedIndex;
367
- if (pubmed.coverage !== "available")
368
- return null;
369
- const RESISTANCE_RX = /\b(resistance|resistant|sensiti[sz]ation|sensiti[sz]er|tolerance|escape mutation)\b/i;
370
- const matches = pubmed.data.results.filter((r) => RESISTANCE_RX.test(r.title));
371
- if (matches.length === 0)
372
- return null;
373
- const rows = matches.slice(0, 50).map((r) => ({
374
- partner_id: null,
375
- partner_name: r.title.slice(0, 80),
376
- predicate: "resistance",
377
- best_score: 0.5,
378
- source_count: 1,
379
- paper_count: 1,
380
- evidence: [
381
- {
382
- source: "pubmed",
383
- predicate: "resistance",
384
- pmid: r.pmid,
385
- excerpt: r.title,
386
- metadata: { year: r.year, journal: r.journal },
387
- },
388
- ],
389
- }));
390
- return rows;
391
- }
392
- export function assembleCombinationEvidence(phase2) {
393
- const pubmed = phase2.phase1.collectors.pubmedIndex;
394
- if (pubmed.coverage !== "available")
395
- return null;
396
- const COMBINATION_RX = /\b(combination|combinations|synergy|synergistic|synergi[sz]e|synergi[sz]ed|synergi[sz]es)\b/i;
397
- const matches = pubmed.data.results.filter((r) => COMBINATION_RX.test(r.title));
398
- if (matches.length === 0)
399
- return null;
400
- const rows = matches.slice(0, 50).map((r) => ({
401
- partner_id: null,
402
- partner_name: r.title.slice(0, 80),
403
- predicate: "combination_effect",
404
- best_score: 0.5,
405
- source_count: 1,
406
- paper_count: 1,
407
- evidence: [
408
- {
409
- source: "pubmed",
410
- predicate: "combination_effect",
411
- pmid: r.pmid,
412
- excerpt: r.title,
413
- metadata: { year: r.year, journal: r.journal },
414
- },
415
- ],
416
- }));
417
- return rows;
418
- }
419
- export function assembleEvidenceConflicts(phase2) {
420
- const ot = phase2.phase1.collectors.opentargets;
421
- if (ot.coverage !== "available")
422
- return null;
423
- const rows = [];
424
- for (const a of ot.data.associations) {
425
- const sources = [
426
- { name: "ot_genetics", v: a.geneticAssociationScore },
427
- { name: "ot_known_drug", v: a.knownDrugScore },
428
- { name: "ot_animal_model", v: a.animalModelScore },
429
- { name: "ot_somatic_mutation", v: a.somaticMutationScore },
430
- { name: "literature", v: a.literatureScore },
431
- ].filter((s) => s.v != null);
432
- if (sources.length < 2)
433
- continue;
434
- const max = Math.max(...sources.map((s) => s.v));
435
- const min = Math.min(...sources.map((s) => s.v));
436
- if (max < 0.5 || min > 0.3)
437
- continue;
438
- const high = sources.find((s) => s.v === max).name;
439
- const low = sources.find((s) => s.v === min).name;
440
- rows.push({
441
- evidence_item_id: `ot:${a.diseaseId}`,
442
- predicate: `${high}=high`,
443
- contradicting_predicates: [`${low}=low`],
444
- surfaced_in_section: "indications",
445
- evidence: makeAssociationEvidence(a),
446
- });
447
- }
448
- if (rows.length === 0)
449
- return null;
450
- return rows.slice(0, 25);
451
- }
452
- export function assembleAdditionalEvidence(phase2) {
453
- const ot = phase2.phase1.collectors.opentargets;
454
- if (ot.coverage !== "available" || ot.data.safetyLiabilities.length === 0) {
455
- return null;
456
- }
457
- const evidence = ot.data.safetyLiabilities.map((s) => ({
458
- source: `opentargets:safety:${s.source}`,
459
- predicate: "safety_liability",
460
- excerpt: `${s.event}${s.effects ? ` (${s.effects})` : ""}`,
461
- metadata: { biosamples: s.biosamples },
462
- }));
463
- return {
464
- rows: [
465
- {
466
- predicate: "safety_liability",
467
- evidence,
468
- },
469
- ],
470
- score_floor: 0.3,
471
- };
472
- }
473
- export function dedupePpiPartners(partners) {
474
- const byId = new Map();
475
- const byNameKey = (s) => s.trim().toLowerCase();
476
- for (const p of partners) {
477
- const id = p.proteinB;
478
- const name = p.proteinB;
479
- if (byId.has(id)) {
480
- const cur = byId.get(id);
481
- cur.string_score = Math.max(cur.string_score ?? 0, p.score);
482
- cur.sources.add("string");
483
- continue;
484
- }
485
- byId.set(id, {
486
- partner_id: id,
487
- partner_name: name,
488
- string_score: p.score,
489
- literature_score: null,
490
- sources: new Set(["string"]),
491
- });
492
- }
493
- const byName = new Map();
494
- for (const v of byId.values()) {
495
- const k = byNameKey(v.partner_name);
496
- const existing = byName.get(k);
497
- if (existing) {
498
- existing.string_score = Math.max(existing.string_score ?? 0, v.string_score ?? 0);
499
- for (const s of v.sources)
500
- existing.sources.add(s);
501
- continue;
502
- }
503
- byName.set(k, v);
504
- }
505
- return [...byName.values()].map((v) => ({
506
- partner_id: v.partner_id,
507
- partner_name: v.partner_name,
508
- string_score: v.string_score,
509
- literature_score: v.literature_score,
510
- combined_score: (v.string_score ?? 0) + (v.literature_score ?? 0),
511
- sources: [...v.sources],
512
- has_human_evidence: true,
513
- has_clinical_evidence: false,
514
- }));
515
- }
516
- export function isClinicalMeasurement(name) {
517
- return MEASUREMENT_TERMS.test(name.trim());
518
- }
519
- const CATEGORY_PATTERNS = [
520
- { category: "safety", rx: /(safety\s+signal|adverse\s+event|toxicity|hepato|cardio|fatal|death)[a-z\s]*?(\.|$)/i },
521
- { category: "efficacy", rx: /(lack\s+of\s+efficacy|did\s+not\s+meet\s+(?:primary|endpoint)|futility)[a-z\s]*?(\.|$)/i },
522
- { category: "strategic", rx: /(strategic|business|pipeline|portfolio|sponsor\s+decision|commercial)[a-z\s]*?(\.|$)/i },
523
- { category: "operational", rx: /(recruit|enroll|funding|covid|pandemic|site\s+closure|investigator|withdraw)[a-z\s]*?(\.|$)/i },
524
- ];
525
- /**
526
- * Match the first known failure-cause pattern against `whyStopped` and return
527
- * the category plus the matched substring (truncated to 200 chars). When no
528
- * pattern matches, falls back to "operational" with the first 200 chars of
529
- * `whyStopped` itself — the schema requires the evidence excerpt to be a
530
- * literal substring of `why_stopped`, which holds by construction.
531
- */
532
- function classifyFailureCategory(whyStopped) {
533
- for (const { category, rx } of CATEGORY_PATTERNS) {
534
- const match = whyStopped.match(rx);
535
- if (match) {
536
- return { category, evidenceExcerpt: match[0].trim().slice(0, 200) };
537
- }
538
- }
539
- return {
540
- category: "operational",
541
- evidenceExcerpt: whyStopped.slice(0, 200),
542
- };
543
- }
544
- /**
545
- * Produce a discriminated `failure_category` block. The "safety" branch
546
- * uses `safety_evidence_excerpt`; all other branches use
547
- * `category_evidence_excerpt`.
548
- */
549
- export function buildFailureCategoryDiscriminated(whyStopped) {
550
- const { category, evidenceExcerpt } = classifyFailureCategory(whyStopped);
551
- if (category === "safety") {
552
- return { category, safety_evidence_excerpt: evidenceExcerpt };
553
- }
554
- return { category, category_evidence_excerpt: evidenceExcerpt };
555
- }
556
- export function isSelfReference(canonicalId, diseaseId, symbol, diseaseName) {
557
- if (canonicalId && diseaseId && canonicalId === diseaseId)
558
- return true;
559
- if (symbol && diseaseName && diseaseName.trim().toUpperCase() === symbol.trim().toUpperCase()) {
560
- return true;
561
- }
562
- return false;
563
- }
564
- export const SOURCE_WEIGHTS = {
565
- literature: 0.05,
566
- ot_genetics: 0.2,
567
- ot_known_drug: 0.25,
568
- ot_animal_model: 0.1,
569
- ot_somatic_mutation: 0.1,
570
- clinvar: 0.15,
571
- };
572
- export function indicationCompositeScore(evidenceScore, sources, paperCount) {
573
- const source_bonuses = {};
574
- let bonusSum = 0;
575
- for (const s of sources) {
576
- const w = SOURCE_WEIGHTS[s];
577
- if (w == null)
578
- continue;
579
- source_bonuses[s] = w;
580
- bonusSum += w;
581
- }
582
- const paper_depth = paperCount <= 0 ? 0 : Math.min(Math.log10(1 + paperCount) * 0.05, 0.15);
583
- const composite = evidenceScore + bonusSum + paper_depth;
584
- return {
585
- composite,
586
- breakdown: { base: evidenceScore, source_bonuses, paper_depth },
587
- };
588
- }
589
- // ── §3.10 helpers ────────────────────────────────────────────────────
590
- const RANK_LEVEL = {
591
- absent: 0,
592
- low: 1,
593
- medium: 2,
594
- high: 3,
595
- };
596
- export function deterministicTranslationalCommentary(phase2) {
597
- const out = [];
598
- const impc = phase2.phase1.collectors.impc;
599
- if (impc.coverage === "available") {
600
- if (impc.data.viability === "lethal_pre_weaning") {
601
- out.push({
602
- severity: "caution",
603
- species: "mouse",
604
- text: "Mouse KO is pre-weaning lethal — therapeutic window depends on partial inhibition, conditional KO, or staged dosing.",
605
- });
606
- }
607
- else if (impc.data.viability === "subviable") {
608
- out.push({
609
- severity: "caution",
610
- species: "mouse",
611
- text: "Mouse KO is sub-viable — full inhibition tolerability uncertain; lead-finding should target partial occupancy or tissue-restricted exposure.",
612
- });
613
- }
614
- if (impc.data.sexDimorphic) {
615
- out.push({
616
- severity: "caution",
617
- species: "mouse",
618
- text: "Mouse KO phenotype is sex-dimorphic — preclinical PK/PD and safety studies must be sex-stratified to avoid masking effects.",
619
- });
620
- }
621
- if (impc.data.organSystems.length >= 3) {
622
- out.push({
623
- severity: "caution",
624
- organ: impc.data.organSystems.slice(0, 3).join(", "),
625
- species: "mouse",
626
- text: `Mouse KO produces phenotypes across ${impc.data.organSystems.length} organ systems — pleiotropy raises the bar for tissue-selective therapeutic windows.`,
627
- });
628
- }
629
- }
630
- const exp = phase2.phase1.collectors.expressionMultiSpecies;
631
- if (exp.coverage === "available") {
632
- const human = exp.data.bySpecies.find((s) => s.species === "homo_sapiens");
633
- if (human && human.tissues.length > 0) {
634
- for (const otherSpecies of exp.data.bySpecies) {
635
- if (otherSpecies.species === "homo_sapiens")
636
- continue;
637
- const speciesLabel = otherSpecies.species.replace("_", " ");
638
- for (const otherTissue of otherSpecies.tissues) {
639
- if (!isSafetyRelevant(otherTissue.tissue))
640
- continue;
641
- const humanTissue = human.tissues.find((t) => t.tissue.toLowerCase() === otherTissue.tissue.toLowerCase());
642
- if (!humanTissue)
643
- continue;
644
- const hLevel = RANK_LEVEL[humanTissue.rank] ?? 0;
645
- const oLevel = RANK_LEVEL[otherTissue.rank] ?? 0;
646
- if (Math.abs(hLevel - oLevel) >= 2) {
647
- out.push({
648
- severity: "caution",
649
- organ: otherTissue.tissue,
650
- species: speciesLabel,
651
- text: `Expression rank diverges by ≥2 steps between human (${humanTissue.rank}) and ${speciesLabel} (${otherTissue.rank}) in ${otherTissue.tissue} — translational extrapolation from this species is unreliable for safety in this organ.`,
652
- });
653
- }
654
- }
655
- }
656
- // Cross-species concordance: human + ≥2 non-human species converge at
657
- // `high` rank in the same safety-relevant tissue. This strengthens
658
- // translational confidence for that organ.
659
- for (const ht of human.tissues) {
660
- if (ht.rank !== "high" || !isSafetyRelevant(ht.tissue))
661
- continue;
662
- const concordantSpecies = [];
663
- for (const other of exp.data.bySpecies) {
664
- if (other.species === "homo_sapiens")
665
- continue;
666
- const match = other.tissues.find((t) => t.tissue.toLowerCase() === ht.tissue.toLowerCase());
667
- if (match && match.rank === "high") {
668
- concordantSpecies.push(other.species.replace("_", " "));
669
- }
670
- }
671
- if (concordantSpecies.length >= 2) {
672
- out.push({
673
- severity: "ok",
674
- organ: ht.tissue,
675
- text: `${ht.tissue} expresses at high rank in human and ${concordantSpecies.join(", ")} — preclinical safety findings in this tissue translate with higher confidence.`,
676
- });
677
- }
678
- }
679
- }
680
- const queried = ["mus_musculus", "rattus_norvegicus", "macaca_mulatta", "canis_lupus_familiaris"];
681
- for (const sp of queried) {
682
- const tissues = exp.data.bySpecies.find((s) => s.species === sp)?.tissues ?? [];
683
- if (tissues.length === 0 && !exp.data.notFound.includes(sp)) {
684
- out.push({
685
- severity: "gap",
686
- species: sp.replace(/_/g, " "),
687
- text: `${sp.replace(/_/g, " ")} expression queried but no tissue calls returned — cross-species coverage is incomplete.`,
688
- });
689
- }
690
- }
691
- }
692
- return out.slice(0, 6);
693
- }
694
- /**
695
- * Partition a list of trials into primary (on-target or unresolved) and
696
- * related (off-target via a related family receptor). Each trial's
697
- * `match_confidence` is derived from ChEMBL mechanism resolution via
698
- * `classifyTrialAttribution`. When no `drugChemblId` is present on an
699
- * intervention (because the upstream ctgov bundle carries only drug names),
700
- * resolution is skipped and the trial lands in primary with confidence "low".
701
- */
702
- export async function partitionTrialsByAttribution(trials, ctx) {
703
- const primary = [];
704
- const related = [];
705
- for (const trial of trials) {
706
- // Normalize interventions: ctgov bundle has string[], typed input may use objects.
707
- const interventions = Array.isArray(trial.interventions)
708
- ? trial.interventions.map((iv) => (typeof iv === "string" ? { drugChemblId: null, name: iv } : iv))
709
- : [];
710
- const result = await classifyTrialAttribution({
711
- assessmentUniprot: ctx.assessmentUniprot,
712
- familyUniprots: ctx.familyUniprots,
713
- interventions,
714
- conditions: trial.conditions ?? [],
715
- drugTargetResolver: ctx.drugTargetResolver,
716
- });
717
- if (result.match_confidence === "off_target") {
718
- related.push({ ...trial, match_confidence: "off_target" });
719
- }
720
- else {
721
- primary.push({ ...trial, match_confidence: result.match_confidence });
722
- }
723
- }
724
- return { primary, related };
725
- }
726
- /**
727
- * Classify why a candidate trial is considered relevant to the assessment.
728
- * Checks interventions against known class drug names first (strongest signal),
729
- * then falls back to title keyword matching, then condition match (weakest).
730
- *
731
- * Rows with match_confidence="low" and kind="condition_match" are unreliable
732
- * (they may be in the same broad disease area but use a completely different
733
- * drug class — e.g., SGLT2 inhibitors appearing in a GLP-1R dossier).
734
- * These rows are dropped by the caller before they reach the dossier.
735
- */
736
- export function classifyRelevanceBasis(trial, knownClassDrugNames, titleKeywordRx) {
737
- // Check interventions against known class drugs (case-insensitive).
738
- for (const iv of trial.interventions ?? []) {
739
- const ivU = iv.toUpperCase();
740
- for (const drug of knownClassDrugNames) {
741
- if (ivU.includes(drug)) {
742
- return { kind: "drug_in_class_match", drug_id: drug, matched_term: iv };
743
- }
744
- }
745
- }
746
- // Check title for target-specific keywords.
747
- const titleMatch = titleKeywordRx.exec(trial.title);
748
- if (titleMatch) {
749
- return { kind: "title_keyword", matched_term: titleMatch[0] };
750
- }
751
- // Fall back to condition match.
752
- return { kind: "condition_match", matched_term: trial.conditions?.[0] };
753
- }
754
- /**
755
- * Classify match confidence for a clinical trial row in the
756
- * `clinical_development.trials` section.
757
- *
758
- * Priority order:
759
- * 1. "high" — gene symbol appears in intervention name or trial title.
760
- * 2. "medium" — intervention name matches a known class drug (drug-in-class).
761
- * 3. "medium" — gene symbol appears in a condition string.
762
- * 4. "low" — no match on any signal.
763
- *
764
- * Drug-in-class matching uses case-insensitive substring comparison against
765
- * `knownClassDrugNames` (uppercase strings from ChEMBL modulators). This
766
- * promotes calcitonin-class interventions (e.g. "Calcitonin-salmon", "SMC021")
767
- * for a CALCR assessment even when the gene symbol itself is absent from the
768
- * intervention name.
769
- */
770
- export function classifyClinicalTrialConfidence(trial, geneSymbol, knownClassDrugNames) {
771
- const symU = geneSymbol.toUpperCase();
772
- const inInterventions = (trial.interventions ?? []).some((i) => i.toUpperCase().includes(symU));
773
- const inTitle = trial.title.toUpperCase().includes(symU);
774
- if (inInterventions || inTitle)
775
- return "high";
776
- // Drug-in-class: check each intervention name against known class drug names.
777
- // Normalize hyphens to spaces before comparison so "Calcitonin-salmon" matches
778
- // the ChEMBL preferred name "CALCITONIN SALMON".
779
- for (const iv of trial.interventions ?? []) {
780
- const ivU = iv.toUpperCase().replace(/-/g, " ");
781
- for (const drug of knownClassDrugNames) {
782
- if (ivU.includes(drug))
783
- return "medium";
784
- }
785
- }
786
- const inConditions = (trial.conditions ?? []).some((c) => c.toUpperCase().includes(symU));
787
- if (inConditions)
788
- return "medium";
789
- return "low";
790
- }
791
- /** Rows the section carries; a candidate set beyond it is reported as truncated. */
792
- const DISCOVERY_TRIAL_CAP = 100;
793
- /** What emptied the discovery-trials section when nothing survived. */
794
- const DISCOVERY_TRIALS_FILTER = "trial already carried by clinical_development.trials, attributed to a related family receptor, matched only by a generic condition string at low confidence, or attribution not eligible for toxicology aggregation";
795
- export async function assembleDiscoveryTrials(phase2, symbol, attrCtx, knownClassDrugNames, attachAttribution) {
796
- const ctgov = phase2.phase1.collectors.ctgov;
797
- if (ctgov.coverage !== "available")
798
- return { coverage: "queried_no_data", error: { message: "ClinicalTrials.gov unavailable" } };
799
- const symU = symbol.toUpperCase();
800
- // Build a case-insensitive title-keyword regex from the target symbol and
801
- // common synonyms embedded in the assessment symbol name. For most targets
802
- // the symbol itself (e.g. "GLP1R", "CALCR") is the right anchor; the regex
803
- // allows optional separators so "GLP-1R" and "GLP 1R" also match.
804
- const escapedSym = symbol.replace(/[-]/g, "[-\\s]?").replace(/\d+/, "\\d+");
805
- const titleKeywordRx = new RegExp(`\\b${escapedSym}\\b`, "i");
806
- // Exclude trials already covered by §2.5a (high-confidence by name/title match).
807
- const candidates = ctgov.data.active.filter((t) => {
808
- const inInterventions = t.interventions.some((i) => i.toUpperCase().includes(symU));
809
- const inTitle = t.title.toUpperCase().includes(symU);
810
- return !(inInterventions || inTitle);
811
- });
812
- const partitioned = candidates.length > 0 ? await partitionTrialsByAttribution(candidates, attrCtx) : { primary: [], related: [] };
813
- // Classify each primary candidate's relevance basis and drop low-confidence
814
- // condition-only matches — these are wrong-class contaminants (e.g., an
815
- // SGLT2 trial picked up because it studies T2D while assessing GLP1R).
816
- const primaryRows = partitioned.primary
817
- .map((t) => {
818
- const relevance_basis = classifyRelevanceBasis(t, knownClassDrugNames, titleKeywordRx);
819
- return {
820
- nct_id: t.nctId,
821
- title: t.title,
822
- phase: t.phase,
823
- status: t.status,
824
- conditions: t.conditions,
825
- // Convert null → undefined to match DiscoveryTrialRowSchema optional fields.
826
- start_date: t.startDate ?? undefined,
827
- completion_date: t.primaryCompletionDate ?? undefined,
828
- match_confidence: t.match_confidence,
829
- relevance_basis,
830
- };
831
- })
832
- .filter((row) => {
833
- // Drop low-confidence rows whose only basis is a generic condition string.
834
- // These fail the DiscoveryTrialRowSchema superRefine invariant and must
835
- // not reach the dossier.
836
- return !(row.match_confidence === "low" && row.relevance_basis.kind === "condition_match");
837
- });
838
- // Trials attributed to a related receptor rather than the assessed target
839
- // are not discovery candidates at all, so they are not carried here.
840
- const carried = primaryRows.slice(0, DISCOVERY_TRIAL_CAP);
841
- const truncatedAway = primaryRows.length - carried.length;
842
- const attributed = carried.map(attachAttribution);
843
- const rows = attributed.filter((r) => r.eligible_for_toxicology_aggregation);
844
- const section = coverageFromFilteredRows({
845
- data: {
846
- rows,
847
- excluded_rows: attributed.filter((r) => !r.eligible_for_toxicology_aggregation),
848
- },
849
- retainedCount: rows.length,
850
- // The cap is a carrying limit, not a judgement about a row, so it is
851
- // reported as truncation rather than counted as a drop.
852
- droppedCount: candidates.length - rows.length - truncatedAway,
853
- filter: DISCOVERY_TRIALS_FILTER,
854
- emptyReason: "every ClinicalTrials.gov record for the target is already carried by clinical_development.trials",
855
- });
856
- return truncatedAway > 0 && section.coverage === "available" ? { ...section, truncated: true } : section;
857
- }
858
- export function assembleEvidenceTimeline(phase2) {
859
- const pubmed = phase2.phase1.collectors.pubmedIndex;
860
- if (pubmed.coverage !== "available" || pubmed.data.results.length === 0) {
861
- return null;
862
- }
863
- const histogram = {};
864
- let first = null;
865
- let last = null;
866
- for (const r of pubmed.data.results) {
867
- const y = parseInt(r.year, 10);
868
- if (!Number.isFinite(y))
869
- continue;
870
- histogram[String(y)] = (histogram[String(y)] ?? 0) + 1;
871
- if (first == null || y < first)
872
- first = y;
873
- if (last == null || y > last)
874
- last = y;
875
- }
876
- const trend_labels = [];
877
- if (first != null && last != null) {
878
- const recent = Object.entries(histogram)
879
- .filter(([y]) => parseInt(y, 10) >= last - 4)
880
- .reduce((s, [, n]) => s + n, 0);
881
- const earlier = Object.entries(histogram)
882
- .filter(([y]) => parseInt(y, 10) >= last - 9 && parseInt(y, 10) < last - 4)
883
- .reduce((s, [, n]) => s + n, 0);
884
- if (recent > earlier * 1.5)
885
- trend_labels.push("accelerating");
886
- else if (recent < earlier * 0.66)
887
- trend_labels.push("decelerating");
888
- else
889
- trend_labels.push("steady");
890
- }
891
- return {
892
- histogram,
893
- first_reported_year: first,
894
- last_reported_year: last,
895
- trend_labels,
896
- };
897
- }
898
- const TIER_ORDER = ["computational", "basic_in_vitro", "complex_in_vitro", "in_vivo_animal", "ex_vivo_human", "clinical"];
899
- export function computeTranslationalChainSummary(tiers) {
900
- const peakEntry = tiers.reduce((best, current) => (best === null || current.claim_count > best.claim_count ? current : best), null);
901
- const peak_evidence_tier = peakEntry && peakEntry.claim_count > 0 ? peakEntry.tier : null;
902
- const clinicalRow = tiers.find((t) => t.tier === "clinical");
903
- const progression_complete = (clinicalRow?.claim_count ?? 0) >= 1;
904
- // First present-tier whose immediate successor in TIER_ORDER has zero claims.
905
- const presentByTier = new Map(tiers.map((t) => [t.tier, t.claim_count > 0]));
906
- let weakest_progression_gap = null;
907
- for (let i = 0; i < TIER_ORDER.length - 1; i++) {
908
- const here = TIER_ORDER[i];
909
- const next = TIER_ORDER[i + 1];
910
- if (presentByTier.get(here) && !presentByTier.get(next)) {
911
- weakest_progression_gap = here;
912
- break;
913
- }
914
- }
915
- return { peak_evidence_tier, progression_complete, weakest_progression_gap };
916
- }
917
- export function assembleTranslationalChain(phase2) {
918
- const pubmed = phase2.phase1.collectors.pubmedIndex;
919
- const ctgov = phase2.phase1.collectors.ctgov;
920
- const impc = phase2.phase1.collectors.impc;
921
- const exp = phase2.phase1.collectors.expressionMultiSpecies;
922
- if (pubmed.coverage !== "available" && ctgov.coverage !== "available" && impc.coverage !== "available") {
923
- return null;
924
- }
925
- const tierCounts = {
926
- computational: { claims: 0, pmids: new Set() },
927
- basic_in_vitro: { claims: 0, pmids: new Set() },
928
- complex_in_vitro: { claims: 0, pmids: new Set() },
929
- in_vivo_animal: { claims: 0, pmids: new Set() },
930
- ex_vivo_human: { claims: 0, pmids: new Set() },
931
- clinical: { claims: 0, pmids: new Set() },
932
- };
933
- if (impc.coverage === "available" && impc.data.phenotypeCount > 0) {
934
- tierCounts.in_vivo_animal.claims += impc.data.phenotypeCount;
935
- }
936
- if (exp.coverage === "available") {
937
- for (const s of exp.data.bySpecies) {
938
- if (s.species !== "homo_sapiens")
939
- tierCounts.in_vivo_animal.claims += s.tissues.length;
940
- }
941
- }
942
- if (pubmed.coverage === "available") {
943
- for (const r of pubmed.data.results) {
944
- const inf = inferSpeciesFromTitle(r.title);
945
- const tier = inf
946
- ? inf.modelSystem === "ex_vivo_human"
947
- ? "ex_vivo_human"
948
- : inf.modelSystem === "complex_in_vitro"
949
- ? "complex_in_vitro"
950
- : "in_vivo_animal"
951
- : "basic_in_vitro";
952
- tierCounts[tier].claims += 1;
953
- tierCounts[tier].pmids.add(r.pmid);
954
- }
955
- }
956
- if (ctgov.coverage === "available") {
957
- tierCounts.clinical.claims += ctgov.data.active.length + ctgov.data.failed.length;
958
- }
959
- const tierRows = TIER_ORDER.map((tier) => ({
960
- tier,
961
- claim_count: tierCounts[tier].claims,
962
- paper_count: tierCounts[tier].pmids.size,
963
- }));
964
- const summary = computeTranslationalChainSummary(tierRows);
965
- return {
966
- tiers: tierRows,
967
- peak_evidence_tier: summary.peak_evidence_tier,
968
- progression_complete: summary.progression_complete,
969
- weakest_progression_gap: summary.weakest_progression_gap,
970
- };
971
- }