@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.
- 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/run-agent.d.ts +1 -1
- 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 +1 -1
- 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,152 +0,0 @@
|
|
|
1
|
-
function normalize(text) {
|
|
2
|
-
return text
|
|
3
|
-
.toUpperCase()
|
|
4
|
-
.replace(/[^A-Z0-9]+/g, " ")
|
|
5
|
-
.trim();
|
|
6
|
-
}
|
|
7
|
-
function tokenMatch(haystack, needle) {
|
|
8
|
-
const h = ` ${normalize(haystack)} `;
|
|
9
|
-
const n = normalize(needle);
|
|
10
|
-
if (!n)
|
|
11
|
-
return false;
|
|
12
|
-
return h.includes(` ${n} `);
|
|
13
|
-
}
|
|
14
|
-
function trialText(trial) {
|
|
15
|
-
return [
|
|
16
|
-
trial.title,
|
|
17
|
-
trial.officialTitle,
|
|
18
|
-
...(trial.conditions ?? []),
|
|
19
|
-
...(trial.interventions ?? []),
|
|
20
|
-
...(trial.interventionDetails ?? []).flatMap((i) => [i.name, i.description, ...(i.otherNames ?? [])]),
|
|
21
|
-
trial.briefSummary,
|
|
22
|
-
trial.detailedDescription,
|
|
23
|
-
]
|
|
24
|
-
.filter((v) => typeof v === "string" && v.length > 0)
|
|
25
|
-
.join(" ");
|
|
26
|
-
}
|
|
27
|
-
function interventionDetails(trial) {
|
|
28
|
-
if (trial.interventionDetails && trial.interventionDetails.length > 0) {
|
|
29
|
-
return trial.interventionDetails;
|
|
30
|
-
}
|
|
31
|
-
return (trial.interventions ?? []).map((name) => ({
|
|
32
|
-
name,
|
|
33
|
-
type: null,
|
|
34
|
-
description: null,
|
|
35
|
-
otherNames: [],
|
|
36
|
-
}));
|
|
37
|
-
}
|
|
38
|
-
function toResolvedIntervention(intervention, source, extras = {}) {
|
|
39
|
-
return {
|
|
40
|
-
name: intervention.name,
|
|
41
|
-
intervention_type: intervention.type ?? null,
|
|
42
|
-
target_uniprots: extras.target_uniprots ?? [],
|
|
43
|
-
resolver_source: source,
|
|
44
|
-
...extras,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
function isDiagnosticOrBiomarkerOnly(trial) {
|
|
48
|
-
const details = interventionDetails(trial);
|
|
49
|
-
const hasOnlyDiagnostic = details.length > 0 && details.every((i) => ["DIAGNOSTIC_TEST", "OTHER", "PROCEDURE"].includes(String(i.type ?? "").toUpperCase()));
|
|
50
|
-
const design = `${trial.studyType ?? ""} ${trial.primaryPurpose ?? ""}`.toUpperCase();
|
|
51
|
-
return hasOnlyDiagnostic || /\bOBSERVATIONAL\b|\bBASIC_SCIENCE\b/.test(design);
|
|
52
|
-
}
|
|
53
|
-
function targetBiomarkerMention(trial, symbol) {
|
|
54
|
-
const text = normalize(trialText(trial));
|
|
55
|
-
const sym = normalize(symbol);
|
|
56
|
-
if (!sym)
|
|
57
|
-
return false;
|
|
58
|
-
const wrapped = ` ${text} `;
|
|
59
|
-
return wrapped.includes(` ${sym} `) || wrapped.includes(`S ${sym} `) || wrapped.includes(` ${sym} LEVEL`) || wrapped.includes(` ${sym} CONCENTRATION`);
|
|
60
|
-
}
|
|
61
|
-
function excluded(relationship, reason, basisKind, source, resolved_interventions) {
|
|
62
|
-
return {
|
|
63
|
-
eligible_for_toxicology_aggregation: false,
|
|
64
|
-
attribution: {
|
|
65
|
-
relationship,
|
|
66
|
-
evidence_role: relationship === "target_biomarker" || relationship === "pathway_biomarker" ? "contextual" : "excluded",
|
|
67
|
-
basis: [{ kind: basisKind, source }],
|
|
68
|
-
resolved_interventions,
|
|
69
|
-
exclusion_reason: reason,
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
export function classifyClinicalEvidenceTrial(trial, ctx) {
|
|
74
|
-
const details = interventionDetails(trial);
|
|
75
|
-
for (const program of ctx.therapeuticPrograms) {
|
|
76
|
-
const matchesProgram = (program.nctIds ?? []).includes(trial.nctId) ||
|
|
77
|
-
details.some((i) => tokenMatch(i.name, program.name) || (i.otherNames ?? []).some((alias) => tokenMatch(alias, program.name))) ||
|
|
78
|
-
tokenMatch(trial.title, program.name);
|
|
79
|
-
const hitsTarget = normalize(program.targetSymbol) === normalize(ctx.assessmentSymbol) || program.targetUniprot === ctx.assessmentUniprot;
|
|
80
|
-
if (matchesProgram && hitsTarget) {
|
|
81
|
-
const matched = details.find((i) => tokenMatch(i.name, program.name)) ??
|
|
82
|
-
details[0] ?? {
|
|
83
|
-
name: program.name,
|
|
84
|
-
type: null,
|
|
85
|
-
description: null,
|
|
86
|
-
otherNames: [],
|
|
87
|
-
};
|
|
88
|
-
return {
|
|
89
|
-
eligible_for_toxicology_aggregation: true,
|
|
90
|
-
attribution: {
|
|
91
|
-
relationship: "direct_modulator",
|
|
92
|
-
evidence_role: "supports_target",
|
|
93
|
-
basis: [
|
|
94
|
-
{
|
|
95
|
-
kind: "therapeutic_program_match",
|
|
96
|
-
source: "therapeutic_programs",
|
|
97
|
-
excerpt: `${program.name} targets ${program.targetSymbol}`,
|
|
98
|
-
},
|
|
99
|
-
],
|
|
100
|
-
resolved_interventions: [
|
|
101
|
-
toResolvedIntervention(matched, "therapeutic_programs", {
|
|
102
|
-
therapeutic_program_id: program.programId,
|
|
103
|
-
target_uniprots: program.targetUniprot ? [program.targetUniprot] : [],
|
|
104
|
-
}),
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
for (const intervention of details) {
|
|
111
|
-
const candidates = [intervention.name, ...(intervention.otherNames ?? [])];
|
|
112
|
-
const matchedDrug = ctx.knownClassDrugs.find((drug) => candidates.some((candidate) => tokenMatch(candidate, drug.name)));
|
|
113
|
-
if (!matchedDrug)
|
|
114
|
-
continue;
|
|
115
|
-
const targetUniprots = matchedDrug.targetUniprots ?? [];
|
|
116
|
-
if (targetUniprots.includes(ctx.assessmentUniprot)) {
|
|
117
|
-
return {
|
|
118
|
-
eligible_for_toxicology_aggregation: true,
|
|
119
|
-
attribution: {
|
|
120
|
-
relationship: "class_modulator",
|
|
121
|
-
evidence_role: "supports_target",
|
|
122
|
-
basis: [{ kind: "known_class_drug", source: "chembl" }],
|
|
123
|
-
resolved_interventions: [
|
|
124
|
-
toResolvedIntervention(intervention, "chembl", {
|
|
125
|
-
chembl_id: matchedDrug.moleculeChemblId ?? null,
|
|
126
|
-
target_uniprots: targetUniprots,
|
|
127
|
-
}),
|
|
128
|
-
],
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
if (targetUniprots.some((u) => ctx.familyUniprots.includes(u))) {
|
|
133
|
-
return excluded("related_family_target", "Intervention targets a related family receptor, not the assessed target.", "related_family_target", "chembl", [
|
|
134
|
-
toResolvedIntervention(intervention, "chembl", {
|
|
135
|
-
chembl_id: matchedDrug.moleculeChemblId ?? null,
|
|
136
|
-
target_uniprots: targetUniprots,
|
|
137
|
-
}),
|
|
138
|
-
]);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
if (targetBiomarkerMention(trial, ctx.assessmentSymbol)) {
|
|
142
|
-
const resolved = details.map((i) => toResolvedIntervention(i, "ctgov"));
|
|
143
|
-
return excluded("target_biomarker", isDiagnosticOrBiomarkerOnly(trial)
|
|
144
|
-
? "Target is used as a biomarker or diagnostic endpoint, not as a therapeutic intervention."
|
|
145
|
-
: "Target is mentioned in biomarker/outcome context without target-modulating intervention evidence.", "biomarker_endpoint", "ctgov", resolved);
|
|
146
|
-
}
|
|
147
|
-
const conditionOnly = (trial.conditions ?? []).some((c) => tokenMatch(c, ctx.assessmentSymbol));
|
|
148
|
-
if (conditionOnly) {
|
|
149
|
-
return excluded("condition_only", "Trial matched only by condition text and has no target-modulating intervention evidence.", "condition_only", "ctgov", details.map((i) => toResolvedIntervention(i, "ctgov")));
|
|
150
|
-
}
|
|
151
|
-
return excluded("unrelated", "No intervention or mechanism evidence links this trial to the assessed target.", "text_match", "ctgov", details.map((i) => toResolvedIntervention(i, "ctgov")));
|
|
152
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { Derived } from "@inflexa-ai/harness/contracts/target-dossier.js";
|
|
2
|
-
import { type OrganSystem } from "../../../contracts/organ-system.js";
|
|
3
|
-
/**
|
|
4
|
-
* Derive the set of organs that should appear in the organ rollup based on
|
|
5
|
-
* available safety signals (FAERS top signals, serious trial AEs, and
|
|
6
|
-
* off-target panel entries).
|
|
7
|
-
*
|
|
8
|
-
* Exported so the Phase-4 assembler can call the same logic when building
|
|
9
|
-
* organ_rollup rows — ensuring the set of "expected" organs is identical at
|
|
10
|
-
* assembly time and at derived-validation time.
|
|
11
|
-
*/
|
|
12
|
-
export declare function expectedOrgansFromBody(bodyInput: unknown): OrganSystem[];
|
|
13
|
-
/**
|
|
14
|
-
* Build the derived sub-tree from a raw dossier body.
|
|
15
|
-
*
|
|
16
|
-
* Pure function: no I/O, no LLM calls, no side effects. Defensive against
|
|
17
|
-
* missing/partial bodies — always returns a best-effort result and never throws.
|
|
18
|
-
* Schema validation happens at the persist boundary, not here.
|
|
19
|
-
*/
|
|
20
|
-
export declare function computeDerivedFields(bodyInput: unknown): Derived;
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import { ORGAN_SYSTEMS } from "../../../contracts/organ-system.js";
|
|
2
|
-
import { classifyOrgan, classifyTrialAe } from "./meddra-organ-map.js";
|
|
3
|
-
import { HIGH_EXPRESSION_TPM_THRESHOLD } from "./expression-constants.js";
|
|
4
|
-
const FAERS_TOP_SIGNAL_THRESHOLD = 1000;
|
|
5
|
-
const ORGAN_SYSTEM_SET = new Set(ORGAN_SYSTEMS);
|
|
6
|
-
/**
|
|
7
|
-
* The body arrives here unvalidated, so an organ token is checked against the
|
|
8
|
-
* vocabulary before it is carried into the derived sub-tree.
|
|
9
|
-
*/
|
|
10
|
-
function isOrganSystem(value) {
|
|
11
|
-
return typeof value === "string" && ORGAN_SYSTEM_SET.has(value);
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Derive the set of organs that should appear in the organ rollup based on
|
|
15
|
-
* available safety signals (FAERS top signals, serious trial AEs, and
|
|
16
|
-
* off-target panel entries).
|
|
17
|
-
*
|
|
18
|
-
* Exported so the Phase-4 assembler can call the same logic when building
|
|
19
|
-
* organ_rollup rows — ensuring the set of "expected" organs is identical at
|
|
20
|
-
* assembly time and at derived-validation time.
|
|
21
|
-
*/
|
|
22
|
-
export function expectedOrgansFromBody(bodyInput) {
|
|
23
|
-
const body = bodyInput;
|
|
24
|
-
const expected = new Set();
|
|
25
|
-
const top = body?.safety_profile?.faers?.data?.top_signals ?? [];
|
|
26
|
-
for (const sig of top) {
|
|
27
|
-
if (sig.report_count >= FAERS_TOP_SIGNAL_THRESHOLD) {
|
|
28
|
-
const organ = classifyOrgan(sig.meddra_term);
|
|
29
|
-
if (organ)
|
|
30
|
-
expected.add(organ);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
const serious = body?.safety_profile?.trial_aes?.data?.serious ?? [];
|
|
34
|
-
for (const ae of serious) {
|
|
35
|
-
const organ = classifyTrialAe(ae);
|
|
36
|
-
if (organ)
|
|
37
|
-
expected.add(organ);
|
|
38
|
-
}
|
|
39
|
-
const offTargets = body?.safety_profile?.off_target_panel?.data?.rows ?? [];
|
|
40
|
-
for (const r of offTargets) {
|
|
41
|
-
if (r.organ_system && isOrganSystem(r.organ_system))
|
|
42
|
-
expected.add(r.organ_system);
|
|
43
|
-
}
|
|
44
|
-
return [...expected].sort();
|
|
45
|
-
}
|
|
46
|
-
function presentOrgans(body) {
|
|
47
|
-
const rows = body?.safety_profile?.organ_rollup?.data?.rows ?? [];
|
|
48
|
-
return rows
|
|
49
|
-
.map((r) => r.organ)
|
|
50
|
-
.filter(isOrganSystem)
|
|
51
|
-
.sort();
|
|
52
|
-
}
|
|
53
|
-
function countDistinctPapers(body) {
|
|
54
|
-
const set = new Set();
|
|
55
|
-
const keyPapers = body?.reference_biology?.key_papers?.data?.rows ?? [];
|
|
56
|
-
for (const p of keyPapers)
|
|
57
|
-
if (p.pmid)
|
|
58
|
-
set.add(String(p.pmid));
|
|
59
|
-
return set.size;
|
|
60
|
-
}
|
|
61
|
-
function countDistinctTrials(body) {
|
|
62
|
-
const set = new Set();
|
|
63
|
-
// Ineligible trials are partitioned into `excluded_rows`, so the distinct
|
|
64
|
-
// count must union both buckets for trials, failed_trials, and
|
|
65
|
-
// discovery_trials. Outcomes preserves every row in `rows` and has no
|
|
66
|
-
// `excluded_rows` bucket.
|
|
67
|
-
const partitioned = [body?.clinical_development?.trials?.data, body?.clinical_development?.failed_trials?.data, body?.analytics?.discovery_trials?.data];
|
|
68
|
-
for (const data of partitioned) {
|
|
69
|
-
for (const bucket of ["rows", "excluded_rows"]) {
|
|
70
|
-
for (const r of data?.[bucket] ?? []) {
|
|
71
|
-
if (r.nct_id)
|
|
72
|
-
set.add(String(r.nct_id));
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
for (const r of body?.clinical_development?.outcomes?.data?.rows ?? []) {
|
|
77
|
-
if (r.nct_id)
|
|
78
|
-
set.add(String(r.nct_id));
|
|
79
|
-
}
|
|
80
|
-
return set.size;
|
|
81
|
-
}
|
|
82
|
-
// Walk the entire body tree counting all `evidence` array entries to produce
|
|
83
|
-
// a total evidence item count. This is intentionally recursive and treats every
|
|
84
|
-
// nested `evidence` array as contributing to the total.
|
|
85
|
-
function countTotalEvidenceItems(body) {
|
|
86
|
-
let total = 0;
|
|
87
|
-
const walk = (v) => {
|
|
88
|
-
if (Array.isArray(v)) {
|
|
89
|
-
for (const item of v)
|
|
90
|
-
walk(item);
|
|
91
|
-
}
|
|
92
|
-
else if (v && typeof v === "object") {
|
|
93
|
-
const node = v;
|
|
94
|
-
if (Array.isArray(node.evidence))
|
|
95
|
-
total += node.evidence.length;
|
|
96
|
-
for (const k of Object.keys(v))
|
|
97
|
-
walk(node[k]);
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
walk(body);
|
|
101
|
-
return total;
|
|
102
|
-
}
|
|
103
|
-
function hasHumanEvidence(body) {
|
|
104
|
-
let found = false;
|
|
105
|
-
const walk = (v) => {
|
|
106
|
-
if (found)
|
|
107
|
-
return;
|
|
108
|
-
if (Array.isArray(v)) {
|
|
109
|
-
for (const item of v)
|
|
110
|
-
walk(item);
|
|
111
|
-
}
|
|
112
|
-
else if (v && typeof v === "object") {
|
|
113
|
-
const node = v;
|
|
114
|
-
if (node.has_human_evidence === true || node.is_human === true) {
|
|
115
|
-
found = true;
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
for (const k of Object.keys(v))
|
|
119
|
-
walk(node[k]);
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
walk(body);
|
|
123
|
-
return found;
|
|
124
|
-
}
|
|
125
|
-
function hasClinicalEvidence(body) {
|
|
126
|
-
let found = false;
|
|
127
|
-
const walk = (v) => {
|
|
128
|
-
if (found)
|
|
129
|
-
return;
|
|
130
|
-
if (Array.isArray(v)) {
|
|
131
|
-
for (const item of v)
|
|
132
|
-
walk(item);
|
|
133
|
-
}
|
|
134
|
-
else if (v && typeof v === "object") {
|
|
135
|
-
const node = v;
|
|
136
|
-
if (node.has_clinical_evidence === true || node.is_clinical === true) {
|
|
137
|
-
found = true;
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
for (const k of Object.keys(v))
|
|
141
|
-
walk(node[k]);
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
walk(body);
|
|
145
|
-
return found;
|
|
146
|
-
}
|
|
147
|
-
function highestRiskOrgan(body) {
|
|
148
|
-
const rows = body?.safety_profile?.organ_rollup?.data?.rows ?? [];
|
|
149
|
-
const high = rows.find((r) => r.risk_level === "high");
|
|
150
|
-
if (high)
|
|
151
|
-
return high.organ;
|
|
152
|
-
const med = rows.find((r) => r.risk_level === "medium");
|
|
153
|
-
if (med)
|
|
154
|
-
return med.organ;
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
function topIndicationSummary(body) {
|
|
158
|
-
const rows = body?.indications?.coverage === "available" ? (body.indications?.data?.rows ?? []) : [];
|
|
159
|
-
if (rows.length === 0) {
|
|
160
|
-
return { highest_score: null, strongest_strength_label: null, highest_score_is_conflicted: false };
|
|
161
|
-
}
|
|
162
|
-
let top = null;
|
|
163
|
-
for (const r of rows) {
|
|
164
|
-
if (typeof r.composite_score !== "number")
|
|
165
|
-
continue;
|
|
166
|
-
if (!top || r.composite_score > top.composite_score)
|
|
167
|
-
top = r;
|
|
168
|
-
}
|
|
169
|
-
if (!top) {
|
|
170
|
-
return { highest_score: null, strongest_strength_label: null, highest_score_is_conflicted: false };
|
|
171
|
-
}
|
|
172
|
-
const conflicted = Array.isArray(top.evidence) ? top.evidence.some((e) => e?.predicate === "direction_mismatch") : false;
|
|
173
|
-
return {
|
|
174
|
-
highest_score: top.composite_score ?? null,
|
|
175
|
-
strongest_strength_label: top.disease_name ?? null,
|
|
176
|
-
highest_score_is_conflicted: conflicted,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
// Count distinct high-expression tissues across the human protein atlas consensus
|
|
180
|
-
// (normal_tissue_expression, keyed as "human") and the cross-species expression
|
|
181
|
-
// heatmap. Deduplication uses `${tissue}::${species}` pairs so the same tissue
|
|
182
|
-
// in different species counts as separate entries.
|
|
183
|
-
function countHighExpressionTissues(body) {
|
|
184
|
-
const set = new Set();
|
|
185
|
-
const consensusRows = body?.reference_biology?.normal_tissue_expression?.data?.rows ?? [];
|
|
186
|
-
for (const r of consensusRows) {
|
|
187
|
-
if ((r.value ?? 0) >= HIGH_EXPRESSION_TPM_THRESHOLD && r.tissue) {
|
|
188
|
-
set.add(`${r.tissue}::human`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
const heatmapCells = body?.reference_biology?.preclinical?.expression_heatmap?.data?.cells ?? [];
|
|
192
|
-
for (const c of heatmapCells) {
|
|
193
|
-
if (c.rank === "high" && c.tissue) {
|
|
194
|
-
const species = c.species ?? "unknown";
|
|
195
|
-
set.add(`${c.tissue}::${species}`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
return set.size;
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Build the derived sub-tree from a raw dossier body.
|
|
202
|
-
*
|
|
203
|
-
* Pure function: no I/O, no LLM calls, no side effects. Defensive against
|
|
204
|
-
* missing/partial bodies — always returns a best-effort result and never throws.
|
|
205
|
-
* Schema validation happens at the persist boundary, not here.
|
|
206
|
-
*/
|
|
207
|
-
export function computeDerivedFields(bodyInput) {
|
|
208
|
-
const body = bodyInput;
|
|
209
|
-
const classLiabilityRows = body?.safety_profile?.class_precedent?.coverage === "available"
|
|
210
|
-
? (body.safety_profile?.class_precedent?.data?.per_organ ?? []).filter((o) => o.is_class_liability)
|
|
211
|
-
: [];
|
|
212
|
-
const classLiabilityCount = classLiabilityRows.length;
|
|
213
|
-
const sameClassDrugCount = body?.safety_profile?.class_precedent?.coverage === "available" ? (body.safety_profile?.class_precedent?.data?.drugs_in_class ?? []).length : 0;
|
|
214
|
-
const offTargetRows = body?.safety_profile?.off_target_panel?.data?.rows ?? [];
|
|
215
|
-
const safetyHits = offTargetRows.filter((r) => r.is_safety_panel_target === true).length;
|
|
216
|
-
const offTissueRows = body?.off_tissue_risk?.data?.rows ?? [];
|
|
217
|
-
const offTissueOrganCount = new Set(offTissueRows.map((r) => r.organ).filter(Boolean)).size;
|
|
218
|
-
const expected = expectedOrgansFromBody(body);
|
|
219
|
-
const present = presentOrgans(body);
|
|
220
|
-
const missing = expected.filter((o) => !present.includes(o));
|
|
221
|
-
const seriousness = body?.safety_profile?.faers?.data?.seriousness;
|
|
222
|
-
const fatalBullet = (body?.liability_summary?.liability_bullets ?? []).some((b) => b?.category === "fatal_post_market");
|
|
223
|
-
const anyFatal = seriousness?.coverage === "available" ? (seriousness.by_seriousness?.death ?? 0) > 0 || fatalBullet : fatalBullet ? true : "unknown";
|
|
224
|
-
const tissueRows = body?.reference_biology?.normal_tissue_expression?.data?.rows ?? [];
|
|
225
|
-
const highTissues = countHighExpressionTissues(body);
|
|
226
|
-
const safetySources = ["organ_rollup", "faers", "trial_aes", "off_target_panel", "failed_trials_safety_lens", "class_precedent"].filter((k) => body?.safety_profile?.[k]?.coverage === "available").length;
|
|
227
|
-
return {
|
|
228
|
-
summary: {
|
|
229
|
-
has_human_evidence: hasHumanEvidence(body),
|
|
230
|
-
has_clinical_evidence: hasClinicalEvidence(body),
|
|
231
|
-
total_evidence_items: countTotalEvidenceItems(body),
|
|
232
|
-
total_distinct_papers: countDistinctPapers(body),
|
|
233
|
-
total_distinct_clinical_trials: countDistinctTrials(body),
|
|
234
|
-
...topIndicationSummary(body),
|
|
235
|
-
},
|
|
236
|
-
risk_summary: {
|
|
237
|
-
any_fatal_signal: anyFatal,
|
|
238
|
-
highest_risk_organ: highestRiskOrgan(body),
|
|
239
|
-
off_target_safety_target_hits: safetyHits,
|
|
240
|
-
class_liability_count: classLiabilityCount,
|
|
241
|
-
},
|
|
242
|
-
liability_summary: {
|
|
243
|
-
counts: {
|
|
244
|
-
class_liability_count: classLiabilityCount,
|
|
245
|
-
safety_target_off_target_count: safetyHits,
|
|
246
|
-
off_tissue_risk_organ_count: offTissueOrganCount,
|
|
247
|
-
},
|
|
248
|
-
expression_breadth: {
|
|
249
|
-
total_assessed_tissues: tissueRows.length,
|
|
250
|
-
high_expression_tissue_count: highTissues,
|
|
251
|
-
},
|
|
252
|
-
same_class_drug_count: sameClassDrugCount,
|
|
253
|
-
safety_data_sources_checked: safetySources,
|
|
254
|
-
},
|
|
255
|
-
organ_rollup_completeness: {
|
|
256
|
-
expected_organs: expected,
|
|
257
|
-
present_organs: present,
|
|
258
|
-
missing_organs: missing,
|
|
259
|
-
},
|
|
260
|
-
};
|
|
261
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export declare function computeSelectivity(input: {
|
|
2
|
-
primary_pchembl: number | null;
|
|
3
|
-
off_target_pchembl: number | null;
|
|
4
|
-
primary_source?: "chembl_target_drug_indication" | "literature_curated";
|
|
5
|
-
}): {
|
|
6
|
-
vs_primary_potency: {
|
|
7
|
-
primary_pchembl_used: number;
|
|
8
|
-
primary_source: "chembl_target_drug_indication" | "literature_curated";
|
|
9
|
-
fold: number;
|
|
10
|
-
log_units: number;
|
|
11
|
-
};
|
|
12
|
-
} | {
|
|
13
|
-
selectivity_unknown: true;
|
|
14
|
-
reason: string;
|
|
15
|
-
};
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export function computeSelectivity(input) {
|
|
2
|
-
if (input.primary_pchembl == null) {
|
|
3
|
-
return { selectivity_unknown: true, reason: "no primary pChEMBL available for this modulator in ChEMBL target-drug-indication table" };
|
|
4
|
-
}
|
|
5
|
-
if (input.off_target_pchembl == null) {
|
|
6
|
-
return { selectivity_unknown: true, reason: "no off-target pChEMBL recorded" };
|
|
7
|
-
}
|
|
8
|
-
const logUnits = input.primary_pchembl - input.off_target_pchembl;
|
|
9
|
-
const fold = Math.pow(10, logUnits);
|
|
10
|
-
return {
|
|
11
|
-
vs_primary_potency: {
|
|
12
|
-
primary_pchembl_used: input.primary_pchembl,
|
|
13
|
-
primary_source: input.primary_source ?? "chembl_target_drug_indication",
|
|
14
|
-
fold,
|
|
15
|
-
log_units: logUnits,
|
|
16
|
-
},
|
|
17
|
-
};
|
|
18
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { type ChemblModulator } from "../../../tools/lib/chembl-client.js";
|
|
2
|
-
export type DroppedSynonym = {
|
|
3
|
-
chemblId: string;
|
|
4
|
-
reason: "same_active_substance" | "ambiguous_name_synonym";
|
|
5
|
-
keptId: string | null;
|
|
6
|
-
};
|
|
7
|
-
export type DedupResult<T extends Pick<ChemblModulator, "moleculeChemblId" | "parentChemblId" | "maxPhase" | "firstApproval">> = {
|
|
8
|
-
kept: T[];
|
|
9
|
-
dropped: DroppedSynonym[];
|
|
10
|
-
};
|
|
11
|
-
/**
|
|
12
|
-
* Group modulators by parent_molecule_chembl_id (or moleculeChemblId when
|
|
13
|
-
* the parent is absent — singleton groups). Within each group keep the
|
|
14
|
-
* "most informative" entry: prefer firstApproval populated, then highest
|
|
15
|
-
* maxPhase, then earliest moleculeChemblId for deterministic tie-break.
|
|
16
|
-
*
|
|
17
|
-
* Reason: ChEMBL exposes the same active substance under multiple
|
|
18
|
-
* molecule_chembl_ids (e.g., CALCITONIN SALMON vs generic CALCITONIN),
|
|
19
|
-
* which causes downstream FAERS queries to double-count the same reports.
|
|
20
|
-
*
|
|
21
|
-
* A second pass handles name-prefix synonyms where ChEMBL does not populate
|
|
22
|
-
* molecule_hierarchy (e.g., CALCITONIN → CALCITONIN HUMAN). Entries with a
|
|
23
|
-
* null firstApproval whose preferredName (uppercased) is a strict
|
|
24
|
-
* word-boundary prefix of another entry's name within the same moleculeType
|
|
25
|
-
* are dropped as same_active_substance (one match) or ambiguous_name_synonym
|
|
26
|
-
* (multiple matches).
|
|
27
|
-
*/
|
|
28
|
-
export declare function dedupModulatorsByParent<T extends Pick<ChemblModulator, "moleculeChemblId" | "parentChemblId" | "maxPhase" | "firstApproval" | "preferredName" | "moleculeType">>(modulators: T[]): DedupResult<T>;
|
|
29
|
-
/**
|
|
30
|
-
* For each modulator whose ChEMBL `moleculeType` is empty or "Unknown",
|
|
31
|
-
* fetch the molecule's standard InChI key from ChEMBL, look it up in
|
|
32
|
-
* PubChem, and apply the heuristic classifier (MW + amide-bond count).
|
|
33
|
-
* Returns a new array of modulators with `moleculeType` replaced when a
|
|
34
|
-
* confident classification emerges; otherwise the original ChEMBL value is
|
|
35
|
-
* preserved. Network failures are isolated per modulator.
|
|
36
|
-
*/
|
|
37
|
-
export declare function resolveModulatorMoleculeType<T extends Pick<ChemblModulator, "moleculeChemblId" | "moleculeType">>(modulators: T[]): Promise<T[]>;
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { getMoleculeInChIKey } from "../../../tools/lib/chembl-client.js";
|
|
2
|
-
import { classifyMoleculeType, getCompoundPropertiesByInChIKey } from "../../../tools/lib/pubchem-client.js";
|
|
3
|
-
import { withHost } from "../../../lib/host-concurrency.js";
|
|
4
|
-
/**
|
|
5
|
-
* Group modulators by parent_molecule_chembl_id (or moleculeChemblId when
|
|
6
|
-
* the parent is absent — singleton groups). Within each group keep the
|
|
7
|
-
* "most informative" entry: prefer firstApproval populated, then highest
|
|
8
|
-
* maxPhase, then earliest moleculeChemblId for deterministic tie-break.
|
|
9
|
-
*
|
|
10
|
-
* Reason: ChEMBL exposes the same active substance under multiple
|
|
11
|
-
* molecule_chembl_ids (e.g., CALCITONIN SALMON vs generic CALCITONIN),
|
|
12
|
-
* which causes downstream FAERS queries to double-count the same reports.
|
|
13
|
-
*
|
|
14
|
-
* A second pass handles name-prefix synonyms where ChEMBL does not populate
|
|
15
|
-
* molecule_hierarchy (e.g., CALCITONIN → CALCITONIN HUMAN). Entries with a
|
|
16
|
-
* null firstApproval whose preferredName (uppercased) is a strict
|
|
17
|
-
* word-boundary prefix of another entry's name within the same moleculeType
|
|
18
|
-
* are dropped as same_active_substance (one match) or ambiguous_name_synonym
|
|
19
|
-
* (multiple matches).
|
|
20
|
-
*/
|
|
21
|
-
export function dedupModulatorsByParent(modulators) {
|
|
22
|
-
const groups = new Map();
|
|
23
|
-
for (const m of modulators) {
|
|
24
|
-
const key = m.parentChemblId ?? m.moleculeChemblId;
|
|
25
|
-
const arr = groups.get(key) ?? [];
|
|
26
|
-
arr.push(m);
|
|
27
|
-
groups.set(key, arr);
|
|
28
|
-
}
|
|
29
|
-
const kept = [];
|
|
30
|
-
const dropped = [];
|
|
31
|
-
for (const group of groups.values()) {
|
|
32
|
-
if (group.length === 1) {
|
|
33
|
-
kept.push(group[0]);
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
const ranked = [...group].sort((a, b) => {
|
|
37
|
-
const aHasApp = a.firstApproval != null ? 1 : 0;
|
|
38
|
-
const bHasApp = b.firstApproval != null ? 1 : 0;
|
|
39
|
-
if (aHasApp !== bHasApp)
|
|
40
|
-
return bHasApp - aHasApp;
|
|
41
|
-
const aPhase = a.maxPhase ?? -1;
|
|
42
|
-
const bPhase = b.maxPhase ?? -1;
|
|
43
|
-
if (aPhase !== bPhase)
|
|
44
|
-
return bPhase - aPhase;
|
|
45
|
-
return a.moleculeChemblId.localeCompare(b.moleculeChemblId);
|
|
46
|
-
});
|
|
47
|
-
const winner = ranked[0];
|
|
48
|
-
kept.push(winner);
|
|
49
|
-
for (const loser of ranked.slice(1)) {
|
|
50
|
-
dropped.push({
|
|
51
|
-
chemblId: loser.moleculeChemblId,
|
|
52
|
-
reason: "same_active_substance",
|
|
53
|
-
keptId: winner.moleculeChemblId,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
// Second pass: name-prefix synonym detection within the same moleculeType.
|
|
58
|
-
// Handles cases where ChEMBL does not populate molecule_hierarchy for
|
|
59
|
-
// unqualified generic names (e.g., "CALCITONIN" as a prefix of
|
|
60
|
-
// "CALCITONIN HUMAN" and "CALCITONIN SALMON"). Only drops entries with
|
|
61
|
-
// null firstApproval — an approved generic name should never be silently
|
|
62
|
-
// dropped.
|
|
63
|
-
const byType = new Map();
|
|
64
|
-
for (const m of kept) {
|
|
65
|
-
const k = (m.moleculeType ?? "").toUpperCase();
|
|
66
|
-
const arr = byType.get(k) ?? [];
|
|
67
|
-
arr.push(m);
|
|
68
|
-
byType.set(k, arr);
|
|
69
|
-
}
|
|
70
|
-
const dropIds = new Set();
|
|
71
|
-
for (const group of byType.values()) {
|
|
72
|
-
for (const shortEntry of group) {
|
|
73
|
-
if (shortEntry.firstApproval != null)
|
|
74
|
-
continue;
|
|
75
|
-
const shortName = (shortEntry.preferredName ?? "").trim().toUpperCase();
|
|
76
|
-
if (!shortName)
|
|
77
|
-
continue;
|
|
78
|
-
const longerMatches = group.filter((other) => {
|
|
79
|
-
if (other === shortEntry)
|
|
80
|
-
return false;
|
|
81
|
-
const longName = (other.preferredName ?? "").trim().toUpperCase();
|
|
82
|
-
if (longName.length <= shortName.length)
|
|
83
|
-
return false;
|
|
84
|
-
if (!longName.startsWith(shortName))
|
|
85
|
-
return false;
|
|
86
|
-
// Require a word-boundary character (whitespace or hyphen) immediately
|
|
87
|
-
// after the prefix — prevents "CALC" from matching "CALCITONIN".
|
|
88
|
-
const boundary = longName.charAt(shortName.length);
|
|
89
|
-
return /[\s-]/.test(boundary);
|
|
90
|
-
});
|
|
91
|
-
if (longerMatches.length === 1) {
|
|
92
|
-
dropIds.add(shortEntry.moleculeChemblId);
|
|
93
|
-
dropped.push({
|
|
94
|
-
chemblId: shortEntry.moleculeChemblId,
|
|
95
|
-
reason: "same_active_substance",
|
|
96
|
-
keptId: longerMatches[0].moleculeChemblId,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
else if (longerMatches.length > 1) {
|
|
100
|
-
dropIds.add(shortEntry.moleculeChemblId);
|
|
101
|
-
dropped.push({
|
|
102
|
-
chemblId: shortEntry.moleculeChemblId,
|
|
103
|
-
reason: "ambiguous_name_synonym",
|
|
104
|
-
keptId: null,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const finalKept = kept.filter((m) => !dropIds.has(m.moleculeChemblId));
|
|
110
|
-
return { kept: finalKept, dropped };
|
|
111
|
-
}
|
|
112
|
-
// ── PubChem-backed molecule_type backfill ─────────────────────────────────────
|
|
113
|
-
function looksUnknown(value) {
|
|
114
|
-
if (!value)
|
|
115
|
-
return true;
|
|
116
|
-
const lowered = value.trim().toLowerCase();
|
|
117
|
-
return lowered === "" || lowered === "unknown";
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* For each modulator whose ChEMBL `moleculeType` is empty or "Unknown",
|
|
121
|
-
* fetch the molecule's standard InChI key from ChEMBL, look it up in
|
|
122
|
-
* PubChem, and apply the heuristic classifier (MW + amide-bond count).
|
|
123
|
-
* Returns a new array of modulators with `moleculeType` replaced when a
|
|
124
|
-
* confident classification emerges; otherwise the original ChEMBL value is
|
|
125
|
-
* preserved. Network failures are isolated per modulator.
|
|
126
|
-
*/
|
|
127
|
-
export async function resolveModulatorMoleculeType(modulators) {
|
|
128
|
-
const candidates = modulators.filter((m) => looksUnknown(m.moleculeType));
|
|
129
|
-
if (candidates.length === 0)
|
|
130
|
-
return modulators;
|
|
131
|
-
const refined = new Map();
|
|
132
|
-
await Promise.all(candidates.map(async (m) => {
|
|
133
|
-
try {
|
|
134
|
-
const inchiKey = await withHost("chembl", () => getMoleculeInChIKey(m.moleculeChemblId));
|
|
135
|
-
if (!inchiKey)
|
|
136
|
-
return;
|
|
137
|
-
const props = await withHost("pubchem", () => getCompoundPropertiesByInChIKey(inchiKey));
|
|
138
|
-
if (!props)
|
|
139
|
-
return;
|
|
140
|
-
const classified = classifyMoleculeType(props);
|
|
141
|
-
if (classified !== "Unknown")
|
|
142
|
-
refined.set(m.moleculeChemblId, classified);
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
// Per-modulator failures must not blank out the others — keep the
|
|
146
|
-
// original ChEMBL annotation.
|
|
147
|
-
}
|
|
148
|
-
}));
|
|
149
|
-
if (refined.size === 0)
|
|
150
|
-
return modulators;
|
|
151
|
-
return modulators.map((m) => {
|
|
152
|
-
const next = refined.get(m.moleculeChemblId);
|
|
153
|
-
if (!next)
|
|
154
|
-
return m;
|
|
155
|
-
return { ...m, moleculeType: next };
|
|
156
|
-
});
|
|
157
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deterministic structural conflict detector.
|
|
3
|
-
*
|
|
4
|
-
* Each rule is a pure function over the dossier body. A row is emitted
|
|
5
|
-
* when the body internally contradicts itself in a way that should be
|
|
6
|
-
* impossible post-pipeline-fix. The post-fix pipeline should produce an
|
|
7
|
-
* empty array for any well-formed body; the rules exist to catch
|
|
8
|
-
* regressions and to surface upstream pipeline bugs.
|
|
9
|
-
*/
|
|
10
|
-
export type StructuralEvidenceConflict = {
|
|
11
|
-
evidence_item_id: string;
|
|
12
|
-
predicate: string;
|
|
13
|
-
contradicting_predicates: string[];
|
|
14
|
-
surfaced_in_section: string;
|
|
15
|
-
evidence: never[];
|
|
16
|
-
};
|
|
17
|
-
export declare function detectStructuralEvidenceConflicts(bodyInput: unknown): StructuralEvidenceConflict[];
|