@inflexa-ai/harness 0.35.7 → 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.
- package/dist/auth/types.d.ts +5 -14
- package/dist/auth/types.js +3 -10
- package/dist/billing/usage-recorder.d.ts +1 -1
- package/dist/contracts/analogy-report.d.ts +1 -1
- package/dist/contracts/analogy-report.js +1 -1
- package/dist/contracts/chat-parts.d.ts +2 -3
- package/dist/contracts/index.d.ts +0 -4
- package/dist/contracts/index.js +0 -2
- package/dist/index.d.ts +0 -7
- package/dist/index.js +0 -21
- package/dist/lib/metrics.d.ts +1 -1
- package/dist/lib/otel-spans.d.ts +0 -2
- package/dist/lib/otel-spans.js +0 -2
- package/dist/loop/genai-spans.d.ts +20 -0
- package/dist/loop/genai-spans.js +53 -0
- package/dist/loop/run-agent.d.ts +1 -1
- package/dist/loop/run-agent.js +9 -4
- package/dist/providers/ai-sdk.js +8 -0
- package/dist/runtime/assemble.d.ts +0 -3
- package/dist/runtime/assemble.js +0 -3
- package/dist/state/index.d.ts +0 -2
- package/dist/state/index.js +0 -1
- package/dist/state/init.js +1 -60
- package/dist/state/purge-analysis.d.ts +1 -2
- package/dist/state/purge-analysis.js +1 -2
- package/dist/tasks/regulatory-corpus-boot-refresh.js +1 -1
- package/dist/tools/bio/gene-preclinical-profile.d.ts +1 -3
- package/dist/tools/bio/gene-preclinical-profile.js +1 -3
- package/dist/tools/lib/chembl-client.d.ts +1 -4
- package/dist/tools/lib/chembl-client.js +1 -4
- package/dist/tools/lib/clinical-benchmarks-client.d.ts +1 -1
- package/dist/tools/lib/clinical-benchmarks-client.js +1 -1
- package/dist/tools/lib/clinical-trials-client.d.ts +0 -3
- package/dist/tools/lib/clinical-trials-client.js +0 -3
- package/dist/tools/lib/identifier-resolver.d.ts +5 -7
- package/dist/tools/lib/identifier-resolver.js +5 -7
- package/dist/tools/lib/iuphar-client.d.ts +1 -3
- package/dist/tools/lib/iuphar-client.js +1 -3
- package/dist/tools/lib/opentargets-client.d.ts +1 -1
- package/dist/tools/lib/opentargets-client.js +1 -1
- package/dist/tools/lib/protein-family-normalize.d.ts +5 -7
- package/dist/tools/lib/protein-family-normalize.js +5 -7
- package/dist/tools/lib/uniprot-client.d.ts +0 -7
- package/dist/tools/lib/uniprot-client.js +2 -9
- package/package.json +9 -8
- package/dist/contracts/target-assessment-row.d.ts +0 -85
- package/dist/contracts/target-assessment-row.js +0 -37
- package/dist/contracts/target-dossier.d.ts +0 -12423
- package/dist/contracts/target-dossier.js +0 -1282
- package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +0 -1
- package/dist/prompts/target-assessment/briefs/executive-recommendation.js +0 -85
- package/dist/prompts/target-assessment/briefs/index.d.ts +0 -4
- package/dist/prompts/target-assessment/briefs/index.js +0 -4
- package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +0 -1
- package/dist/prompts/target-assessment/briefs/liability-bullets.js +0 -114
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +0 -1
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +0 -37
- package/dist/prompts/target-assessment/briefs/translational-commentary.d.ts +0 -1
- package/dist/prompts/target-assessment/briefs/translational-commentary.js +0 -23
- package/dist/prompts/target-assessment/drugs-in-class.d.ts +0 -1
- package/dist/prompts/target-assessment/drugs-in-class.js +0 -29
- package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +0 -1
- package/dist/prompts/target-assessment/investigation/adversarial-critique.js +0 -67
- package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +0 -1
- package/dist/prompts/target-assessment/investigation/claim-reverification.js +0 -49
- package/dist/prompts/target-assessment/investigation/index.d.ts +0 -3
- package/dist/prompts/target-assessment/investigation/index.js +0 -3
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +0 -1
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +0 -49
- package/dist/prompts/target-assessment/modulator-triage.d.ts +0 -1
- package/dist/prompts/target-assessment/modulator-triage.js +0 -31
- package/dist/prompts/target-assessment/off-target-annotator.d.ts +0 -1
- package/dist/prompts/target-assessment/off-target-annotator.js +0 -28
- package/dist/prompts/target-assessment/tox-voice/core.d.ts +0 -1
- package/dist/prompts/target-assessment/tox-voice/core.js +0 -74
- package/dist/prompts/target-assessment/tox-voice/critique.d.ts +0 -7
- package/dist/prompts/target-assessment/tox-voice/critique.js +0 -109
- package/dist/prompts/target-assessment/tox-voice/exemplars.d.ts +0 -8
- package/dist/prompts/target-assessment/tox-voice/exemplars.js +0 -24
- package/dist/prompts/target-assessment/tox-voice/index.d.ts +0 -4
- package/dist/prompts/target-assessment/tox-voice/index.js +0 -4
- package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +0 -18
- package/dist/prompts/target-assessment/tox-voice/vocabulary.js +0 -61
- package/dist/state/target-assessments.d.ts +0 -68
- package/dist/state/target-assessments.js +0 -222
- package/dist/workflows/execute-target-assessment.d.ts +0 -118
- package/dist/workflows/execute-target-assessment.js +0 -631
- package/dist/workflows/target-assessment/assemblers/index.d.ts +0 -5
- package/dist/workflows/target-assessment/assemblers/index.js +0 -4
- package/dist/workflows/target-assessment/assemblers/literature.d.ts +0 -281
- package/dist/workflows/target-assessment/assemblers/literature.js +0 -971
- package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +0 -118
- package/dist/workflows/target-assessment/assemblers/orchestrator.js +0 -945
- package/dist/workflows/target-assessment/assemblers/safety.d.ts +0 -374
- package/dist/workflows/target-assessment/assemblers/safety.js +0 -1068
- package/dist/workflows/target-assessment/assemblers/trials.d.ts +0 -30
- package/dist/workflows/target-assessment/assemblers/trials.js +0 -75
- package/dist/workflows/target-assessment/collectors/index.d.ts +0 -114
- package/dist/workflows/target-assessment/collectors/index.js +0 -538
- package/dist/workflows/target-assessment/coverage.d.ts +0 -97
- package/dist/workflows/target-assessment/coverage.js +0 -86
- package/dist/workflows/target-assessment/decisions/index.d.ts +0 -86
- package/dist/workflows/target-assessment/decisions/index.js +0 -127
- package/dist/workflows/target-assessment/fanout/index.d.ts +0 -32
- package/dist/workflows/target-assessment/fanout/index.js +0 -156
- package/dist/workflows/target-assessment/investigation/index.d.ts +0 -97
- package/dist/workflows/target-assessment/investigation/index.js +0 -419
- package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +0 -73
- package/dist/workflows/target-assessment/lib/approval-precedents.js +0 -185
- package/dist/workflows/target-assessment/lib/citation-validator.d.ts +0 -46
- package/dist/workflows/target-assessment/lib/citation-validator.js +0 -316
- package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +0 -65
- package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.js +0 -174
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +0 -44
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -152
- package/dist/workflows/target-assessment/lib/compute-derived.d.ts +0 -20
- package/dist/workflows/target-assessment/lib/compute-derived.js +0 -261
- package/dist/workflows/target-assessment/lib/compute-selectivity.d.ts +0 -15
- package/dist/workflows/target-assessment/lib/compute-selectivity.js +0 -18
- package/dist/workflows/target-assessment/lib/dedup-modulators.d.ts +0 -37
- package/dist/workflows/target-assessment/lib/dedup-modulators.js +0 -157
- package/dist/workflows/target-assessment/lib/evidence-conflict-detector.d.ts +0 -17
- package/dist/workflows/target-assessment/lib/evidence-conflict-detector.js +0 -100
- package/dist/workflows/target-assessment/lib/expression-constants.d.ts +0 -21
- package/dist/workflows/target-assessment/lib/expression-constants.js +0 -21
- package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +0 -20
- package/dist/workflows/target-assessment/lib/family-complex-supplement.js +0 -36
- package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +0 -72
- package/dist/workflows/target-assessment/lib/fda-label-safety.js +0 -166
- package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -30
- package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -44
- package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +0 -34
- package/dist/workflows/target-assessment/lib/hpo-organ-map.js +0 -63
- package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +0 -19
- package/dist/workflows/target-assessment/lib/impc-organ-map.js +0 -39
- package/dist/workflows/target-assessment/lib/intended-polypharm-filter.d.ts +0 -4
- package/dist/workflows/target-assessment/lib/intended-polypharm-filter.js +0 -24
- package/dist/workflows/target-assessment/lib/llm-step.d.ts +0 -80
- package/dist/workflows/target-assessment/lib/llm-step.js +0 -101
- package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +0 -40
- package/dist/workflows/target-assessment/lib/meddra-organ-map.js +0 -138
- package/dist/workflows/target-assessment/lib/recommendation-quality-gates.d.ts +0 -17
- package/dist/workflows/target-assessment/lib/recommendation-quality-gates.js +0 -99
- package/dist/workflows/target-assessment/lib/recommendation-sanitizer.d.ts +0 -2
- package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +0 -51
- package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +0 -25
- package/dist/workflows/target-assessment/lib/regulatory-actions.js +0 -176
- package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +0 -44
- package/dist/workflows/target-assessment/lib/safety-corroboration.js +0 -273
- package/dist/workflows/target-assessment/lib/structured-llm.d.ts +0 -61
- package/dist/workflows/target-assessment/lib/structured-llm.js +0 -102
- package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +0 -59
- package/dist/workflows/target-assessment/lib/target-identity-filter.js +0 -95
- package/dist/workflows/target-assessment/lib/therapeutic-programs.d.ts +0 -20
- package/dist/workflows/target-assessment/lib/therapeutic-programs.js +0 -68
- package/dist/workflows/target-assessment/metrics.d.ts +0 -16
- package/dist/workflows/target-assessment/metrics.js +0 -30
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +0 -3182
- package/dist/workflows/target-assessment/phase4-assemble.js +0 -39
- package/dist/workflows/target-assessment/phase5-persist.d.ts +0 -59
- package/dist/workflows/target-assessment/phase5-persist.js +0 -349
- package/dist/workflows/target-assessment/progress-stream.d.ts +0 -75
- package/dist/workflows/target-assessment/progress-stream.js +0 -110
- package/dist/workflows/target-assessment/progress.d.ts +0 -39
- package/dist/workflows/target-assessment/progress.js +0 -77
- package/dist/workflows/target-assessment/schemas.d.ts +0 -970
- package/dist/workflows/target-assessment/schemas.js +0 -318
- package/dist/workflows/target-assessment/steps/collectors/pathways.d.ts +0 -15
- package/dist/workflows/target-assessment/steps/collectors/pathways.js +0 -58
- package/dist/workflows/target-assessment/steps/fanout/aes-for-one-class-drug-step.d.ts +0 -34
- package/dist/workflows/target-assessment/steps/fanout/aes-for-one-class-drug-step.js +0 -31
- package/dist/workflows/target-assessment/steps/fanout/aes-for-one-trial-step.d.ts +0 -57
- package/dist/workflows/target-assessment/steps/fanout/aes-for-one-trial-step.js +0 -54
- package/dist/workflows/target-assessment/steps/fanout/faers-for-one-modulator-step.d.ts +0 -35
- package/dist/workflows/target-assessment/steps/fanout/faers-for-one-modulator-step.js +0 -32
- package/dist/workflows/target-assessment/steps/fanout/index.d.ts +0 -14
- package/dist/workflows/target-assessment/steps/fanout/index.js +0 -10
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +0 -29
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +0 -29
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +0 -657
- package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -34
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +0 -806
- package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -16
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -28
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -27
- package/dist/workflows/target-assessment/sub-workflows/index.d.ts +0 -15
- package/dist/workflows/target-assessment/sub-workflows/index.js +0 -11
- package/dist/workflows/target-assessment/sub-workflows/per-class-drug-aes.d.ts +0 -41
- package/dist/workflows/target-assessment/sub-workflows/per-class-drug-aes.js +0 -12
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-faers.d.ts +0 -41
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-faers.js +0 -12
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +0 -37
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.js +0 -12
- package/dist/workflows/target-assessment/sub-workflows/per-trial-aes.d.ts +0 -66
- package/dist/workflows/target-assessment/sub-workflows/per-trial-aes.js +0 -12
- package/dist/workflows/target-assessment/synthesis/index.d.ts +0 -607
- package/dist/workflows/target-assessment/synthesis/index.js +0 -354
- package/dist/workflows/target-assessment/synthesis-probes.d.ts +0 -56
- 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
|
-
}
|