@inflexa-ai/harness 0.17.0 → 0.18.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/agents/report-builder.d.ts +2 -0
- package/dist/agents/report-builder.js +2 -0
- package/dist/app/spawn-report-session.d.ts +85 -0
- package/dist/app/spawn-report-session.js +83 -0
- package/dist/contracts/chat-events.d.ts +9 -0
- package/dist/contracts/index.d.ts +6 -2
- package/dist/contracts/index.js +3 -1
- package/dist/contracts/organ-system.d.ts +49 -0
- package/dist/contracts/organ-system.js +67 -0
- package/dist/contracts/report-blocks.d.ts +564 -0
- package/dist/contracts/report-blocks.js +117 -0
- package/dist/contracts/report-reference.d.ts +459 -0
- package/dist/contracts/report-reference.js +256 -0
- package/dist/contracts/schemas/chat-events.d.ts +2 -0
- package/dist/contracts/schemas/chat-events.js +2 -0
- package/dist/contracts/severity.d.ts +15 -0
- package/dist/contracts/severity.js +10 -0
- package/dist/contracts/target-dossier.d.ts +8675 -12972
- package/dist/contracts/target-dossier.js +534 -591
- package/dist/data/safety-panel-schema.d.ts +18 -5
- package/dist/data/safety-panel-schema.js +4 -4
- package/dist/execution/artifact-metadata.js +1 -0
- package/dist/execution/report-runner.js +1 -3
- package/dist/execution/run-synthesis.js +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +19 -0
- package/dist/lib/async-utils.d.ts +16 -4
- package/dist/lib/async-utils.js +20 -15
- package/dist/lib/host-concurrency.js +1 -0
- package/dist/loop/__fixtures__/scripted-provider.js +23 -2
- package/dist/loop/run-agent.js +58 -10
- package/dist/loop/tool-detail.d.ts +40 -0
- package/dist/loop/tool-detail.js +35 -3
- package/dist/loop/types.d.ts +13 -0
- package/dist/memory/thread-history.d.ts +10 -0
- package/dist/memory/thread-history.js +12 -1
- package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/executive-recommendation.js +5 -5
- package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/liability-bullets.js +1 -1
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +11 -3
- package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/adversarial-critique.js +67 -0
- package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/claim-reverification.js +49 -0
- package/dist/prompts/target-assessment/investigation/index.d.ts +3 -0
- package/dist/prompts/target-assessment/investigation/index.js +3 -0
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +49 -0
- package/dist/prompts/target-assessment/tox-voice/index.d.ts +1 -1
- package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +8 -5
- package/dist/prompts/target-assessment/tox-voice/vocabulary.js +9 -20
- package/dist/providers/__fixtures__/session.d.ts +5 -4
- package/dist/providers/__fixtures__/session.js +4 -4
- package/dist/report-model/block-walk.d.ts +56 -0
- package/dist/report-model/block-walk.js +89 -0
- package/dist/report-model/draft-finish.d.ts +60 -0
- package/dist/report-model/draft-finish.js +56 -0
- package/dist/report-model/draft-operations.d.ts +125 -0
- package/dist/report-model/draft-operations.js +341 -0
- package/dist/report-model/draft-read.d.ts +59 -0
- package/dist/report-model/draft-read.js +109 -0
- package/dist/report-model/draft.d.ts +51 -0
- package/dist/report-model/draft.js +32 -0
- package/dist/report-model/fixture-resolver.d.ts +8 -0
- package/dist/report-model/fixture-resolver.js +252 -0
- package/dist/report-model/mint-snapshot.d.ts +30 -0
- package/dist/report-model/mint-snapshot.js +40 -0
- package/dist/report-model/reference-resolver.d.ts +113 -0
- package/dist/report-model/reference-resolver.js +60 -0
- package/dist/report-model/structural-validation.d.ts +22 -0
- package/dist/report-model/structural-validation.js +68 -0
- package/dist/report-model/validate.d.ts +40 -0
- package/dist/report-model/validate.js +80 -0
- package/dist/report-render/chart.d.ts +33 -0
- package/dist/report-render/chart.js +538 -0
- package/dist/report-render/page.d.ts +127 -0
- package/dist/report-render/page.js +307 -0
- package/dist/report-render/references.d.ts +32 -0
- package/dist/report-render/references.js +44 -0
- package/dist/report-render/render.d.ts +25 -0
- package/dist/report-render/render.js +125 -0
- package/dist/report-render/script-json.d.ts +13 -0
- package/dist/report-render/script-json.js +15 -0
- package/dist/report-render/types.d.ts +39 -0
- package/dist/report-render/types.js +10 -0
- package/dist/report-render/views/chart-view.d.ts +11 -0
- package/dist/report-render/views/chart-view.js +23 -0
- package/dist/report-render/views/page-view.d.ts +20 -0
- package/dist/report-render/views/page-view.js +49 -0
- package/dist/report-render/views/prose.d.ts +28 -0
- package/dist/report-render/views/prose.js +75 -0
- package/dist/report-render/views/references-view.d.ts +20 -0
- package/dist/report-render/views/references-view.js +64 -0
- package/dist/report-render/views/values.d.ts +42 -0
- package/dist/report-render/views/values.js +43 -0
- package/dist/runtime/boot.d.ts +3 -3
- package/dist/runtime/boot.js +15 -4
- package/dist/runtime/dbos.d.ts +4 -4
- package/dist/state/artifacts.d.ts +16 -0
- package/dist/state/artifacts.js +18 -0
- package/dist/state/init.js +31 -0
- package/dist/state/purge-analysis.js +1 -0
- package/dist/state/report-versions.d.ts +132 -0
- package/dist/state/report-versions.js +194 -0
- package/dist/tasks/data-profile.js +1 -0
- package/dist/tools/bio/chembl.js +1 -0
- package/dist/tools/bio/comptox.js +1 -0
- package/dist/tools/bio/drug-gene-interactions.js +1 -0
- package/dist/tools/bio/gene-disease-evidence.js +1 -0
- package/dist/tools/bio/gene-preclinical-profile.js +1 -0
- package/dist/tools/bio/keys.d.ts +3 -2
- package/dist/tools/bio/keys.js +3 -2
- package/dist/tools/bio/lookup-annotation.js +1 -0
- package/dist/tools/bio/opentargets.d.ts +3 -3
- package/dist/tools/bio/opentargets.js +4 -3
- package/dist/tools/bio/pubchem.js +1 -0
- package/dist/tools/bio/search-clinical-trials.js +1 -0
- package/dist/tools/bio/search-faers.js +1 -0
- package/dist/tools/bio/search-geo-datasets.js +1 -0
- package/dist/tools/bio/search-interactions.js +1 -0
- package/dist/tools/bio/target-safety.d.ts +4 -6
- package/dist/tools/bio/target-safety.js +7 -6
- package/dist/tools/define-tool.d.ts +19 -12
- package/dist/tools/define-tool.js +4 -2
- package/dist/tools/display/show-user.js +1 -0
- package/dist/tools/iterate-report.js +2 -0
- package/dist/tools/lib/chembl-client.d.ts +2 -0
- package/dist/tools/lib/chembl-client.js +1 -0
- package/dist/tools/lib/monarch-client.d.ts +42 -0
- package/dist/tools/lib/monarch-client.js +100 -0
- package/dist/tools/report/build-report.js +1 -0
- package/dist/tools/report/mint-preview-url.js +1 -0
- package/dist/tools/report/preview-snapshot.js +1 -0
- package/dist/tools/report/submit-report.js +1 -0
- package/dist/tools/report/version-fs.js +4 -0
- package/dist/tools/report-authoring/authoring-tools.d.ts +134 -0
- package/dist/tools/report-authoring/authoring-tools.js +352 -0
- package/dist/tools/research/context7-docs.js +2 -0
- package/dist/tools/research/generate-analogy-report.js +1 -0
- package/dist/tools/research/generate-plan.js +3 -0
- package/dist/tools/research/inspect-data-profile.js +4 -0
- package/dist/tools/research/literature-reviewer.js +1 -0
- package/dist/tools/research/search-arxiv.js +1 -0
- package/dist/tools/research/search-github-repos.js +1 -0
- package/dist/tools/research/search-semantic-scholar.js +1 -0
- package/dist/tools/sandbox/list-available-packages.js +23 -0
- package/dist/tools/sandbox/list-available-refs.js +36 -0
- package/dist/tools/sandbox/report-blocker.js +1 -0
- package/dist/tools/sandbox/skills.js +2 -0
- package/dist/tools/workspace/file-stat.js +1 -0
- package/dist/tools/workspace/show-file.js +2 -0
- package/dist/tools/workspace/show-plan.js +1 -0
- package/dist/workflows/execute-analysis.d.ts +5 -1
- package/dist/workflows/execute-analysis.js +5 -5
- package/dist/workflows/execute-target-assessment.d.ts +13 -2
- package/dist/workflows/execute-target-assessment.js +78 -16
- package/dist/workflows/target-assessment/assemblers/index.d.ts +1 -1
- package/dist/workflows/target-assessment/assemblers/index.js +1 -1
- package/dist/workflows/target-assessment/assemblers/literature.d.ts +9 -27
- package/dist/workflows/target-assessment/assemblers/literature.js +32 -48
- package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +17 -9
- package/dist/workflows/target-assessment/assemblers/orchestrator.js +195 -85
- package/dist/workflows/target-assessment/assemblers/safety.d.ts +70 -48
- package/dist/workflows/target-assessment/assemblers/safety.js +125 -42
- package/dist/workflows/target-assessment/collectors/index.d.ts +5 -2
- package/dist/workflows/target-assessment/collectors/index.js +48 -5
- package/dist/workflows/target-assessment/coverage.d.ts +43 -27
- package/dist/workflows/target-assessment/coverage.js +33 -29
- package/dist/workflows/target-assessment/fanout/index.js +1 -0
- package/dist/workflows/target-assessment/investigation/index.d.ts +97 -0
- package/dist/workflows/target-assessment/investigation/index.js +419 -0
- package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +26 -5
- package/dist/workflows/target-assessment/lib/approval-precedents.js +95 -29
- package/dist/workflows/target-assessment/lib/citation-validator.js +1 -1
- package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +2 -2
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +2 -3
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -4
- package/dist/workflows/target-assessment/lib/compute-derived.d.ts +6 -5
- package/dist/workflows/target-assessment/lib/compute-derived.js +14 -5
- package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +5 -6
- package/dist/workflows/target-assessment/lib/family-complex-supplement.js +2 -3
- package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +72 -0
- package/dist/workflows/target-assessment/lib/fda-label-safety.js +166 -0
- package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -9
- package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -10
- package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +34 -0
- package/dist/workflows/target-assessment/lib/hpo-organ-map.js +63 -0
- package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +19 -0
- package/dist/workflows/target-assessment/lib/impc-organ-map.js +39 -0
- package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +24 -14
- package/dist/workflows/target-assessment/lib/meddra-organ-map.js +59 -47
- package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +1 -1
- package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +2 -3
- package/dist/workflows/target-assessment/lib/regulatory-actions.js +33 -4
- package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +44 -0
- package/dist/workflows/target-assessment/lib/safety-corroboration.js +273 -0
- package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +5 -7
- package/dist/workflows/target-assessment/lib/target-identity-filter.js +5 -7
- package/dist/workflows/target-assessment/metrics.d.ts +1 -6
- package/dist/workflows/target-assessment/metrics.js +1 -13
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +2147 -807
- package/dist/workflows/target-assessment/phase4-assemble.js +19 -19
- package/dist/workflows/target-assessment/phase5-persist.d.ts +20 -21
- package/dist/workflows/target-assessment/phase5-persist.js +56 -173
- package/dist/workflows/target-assessment/schemas.d.ts +46 -1
- package/dist/workflows/target-assessment/schemas.js +19 -0
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +1 -0
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +2 -0
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +26 -62
- package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -4
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +27 -154
- package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -6
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -6
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -18
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +1 -0
- package/dist/workflows/target-assessment/synthesis/index.d.ts +52 -5
- package/dist/workflows/target-assessment/synthesis/index.js +4 -1
- package/dist/workflows/target-assessment/synthesis-probes.d.ts +3 -3
- package/package.json +4 -1
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MedDRA terms and CT.gov system-organ classes onto the canonical organ
|
|
3
|
+
* vocabulary.
|
|
4
|
+
*
|
|
5
|
+
* The values are the canonical tokens, so this module declares no organ
|
|
6
|
+
* enumeration of its own. Every token in the vocabulary is reachable through
|
|
7
|
+
* the rules below, so nothing a producer may legitimately attribute is
|
|
8
|
+
* unreachable by accident.
|
|
9
|
+
*
|
|
10
|
+
* A term resolves onto the organ it names, or onto nothing. A term whose
|
|
11
|
+
* compartment is genuinely ambiguous — "Paraesthesia" and the other nervous
|
|
12
|
+
* system terms that name no compartment, "Adrenal insufficiency" and the other
|
|
13
|
+
* endocrine terms outside the thyroid — matches no rule, so the caller drops
|
|
14
|
+
* and counts it rather than filing it under a neighbour.
|
|
15
|
+
*/
|
|
1
16
|
/**
|
|
2
17
|
* Order matters: more-specific patterns first. GI is matched before
|
|
3
18
|
* "metabolic" because terms like "blood glucose" should land in
|
|
@@ -11,8 +26,9 @@
|
|
|
11
26
|
const RULES = [
|
|
12
27
|
// pancreas (specific) — must come before generic GI
|
|
13
28
|
{ rx: /\bpancreatit|\bamylase|\blipase/i, organ: "pancreas" },
|
|
14
|
-
// thyroid / C-cell
|
|
15
|
-
|
|
29
|
+
// thyroid / C-cell. The token names the thyroid, not the endocrine system,
|
|
30
|
+
// so a non-thyroid endocrine term deliberately matches nothing here.
|
|
31
|
+
{ rx: /thyroid|\b(c[-\s]?cell|calcitonin|goit[re]+|medullar[a-z]*\s*carcinoma)/i, organ: "endocrine_thyroid" },
|
|
16
32
|
// gastrointestinal
|
|
17
33
|
{ rx: /\b(nause|vomit|diarrhoe|diarrhea|constipat|ileus|gastropares|abdominal\s*pain|cholecystit|cholelithias|gallbladder|colit|heartburn)/i, organ: "gi" },
|
|
18
34
|
// hepatic — liver/alt/ast forms added; alt/ast pinned to "increased" to avoid
|
|
@@ -27,23 +43,48 @@ const RULES = [
|
|
|
27
43
|
// cardiac — cardi(ac|al|o) covers CARDIOMYOPATHY/CARDIOVASCULAR/CARDIOTOXICITY;
|
|
28
44
|
// bare heart covers HEART FAILURE, HEART RATE INCREASED, etc.
|
|
29
45
|
{ rx: /\b(cardi(?:ac|al|o)|heart|myocard|tachycard|brady\w*card|arrhythm|atrial\s+fibrillat|qt\s+prolong|torsade)/i, organ: "cardiac" },
|
|
46
|
+
// vascular — placed after cardiac so a myocardial event stays cardiac.
|
|
47
|
+
// `thrombos` rather than the `thrombo` stem, which would swallow
|
|
48
|
+
// THROMBOCYTOPENIA; `\bvascul` never matches inside CEREBROVASCULAR.
|
|
49
|
+
{ rx: /\b(hypertens|hypotens|vasculit|vascular|thrombos|thromboemboli|thrombophleb|embolism|aneurysm|varicose|phlebit)/i, organ: "vascular" },
|
|
30
50
|
// metabolic (glucose, weight, lipid)
|
|
31
51
|
// triglyc has no leading \b because it appears mid-word in HYPERTRIGLYCERIDAEMIA
|
|
32
52
|
{ rx: /\b(glucose|glycaemi|hba1c|insulin|weight\s+(?:decreased|increased)|lipid|cholester|dehydration)|triglyc/i, organ: "metabolic" },
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
{ rx: /\b(
|
|
53
|
+
// respiratory — above the neurological rules so RESPIRATORY DEPRESSION is
|
|
54
|
+
// read as a respiratory event rather than by the bare "depression" stem
|
|
55
|
+
{ rx: /\b(nasopharyng|pharyng|bronch|pneumon|dyspnoe|dyspnea|cough|respiratory|sinusit|pulmonary)/i, organ: "respiratory" },
|
|
56
|
+
// hematologic — above the neurological rules for the same reason, so
|
|
57
|
+
// BONE MARROW DEPRESSION is read as haematologic
|
|
58
|
+
{ rx: /\b(haemoglobin|hemoglobin|haematocrit|leuko|neutro|thrombocyt|anaemi|haemat|platelet|marrow)/i, organ: "hematologic" },
|
|
59
|
+
// ocular — above the nerve rules so OPTIC NEUROPATHY stays ocular.
|
|
60
|
+
// `macular` is pinned to its ocular compounds so it never matches
|
|
61
|
+
// MACULOPAPULAR RASH.
|
|
62
|
+
{
|
|
63
|
+
rx: /\b(ocular|ophthalm|retinopath|retinal|glaucoma|cataract|uveit|conjunctivit|blindness|vision|visual|keratit|scleriti|blephariti|eye\s|macular\s+(?:oedema|edema|degenerat)|optic\s+(?:neuropath|neurit|atroph))/i,
|
|
64
|
+
organ: "ocular",
|
|
65
|
+
},
|
|
66
|
+
// peripheral nervous system — a neuropathy is a disease of the peripheral
|
|
67
|
+
// nerves; a central lesion is named by the terms in the cns rule below
|
|
68
|
+
// `neuropath` carries no leading \b so POLYNEUROPATHY and MONONEUROPATHY
|
|
69
|
+
// resolve as well as NEUROPATHY PERIPHERAL
|
|
70
|
+
{ rx: /neuropath|neurit|\b(radiculopath|neuralg|carpal\s+tunnel|guillain|plexopath|peripheral\s+(?:motor|sensory))/i, organ: "pns" },
|
|
71
|
+
// central nervous system
|
|
72
|
+
{
|
|
73
|
+
rx: /\b(headache|dizz|seizure|convuls|suici|depression|anxiet|encephalo|migrain|myelopath|meningit|cerebr|intracranial|somnolen|ataxi|tremor|stroke)/i,
|
|
74
|
+
organ: "cns",
|
|
75
|
+
},
|
|
39
76
|
// dermatologic
|
|
40
77
|
{ rx: /\b(rash|eczem|prurit|urticari|injection\s+site\s+(?:pain|haemorrhag|erythem))/i, organ: "dermatologic" },
|
|
41
|
-
// musculoskeletal
|
|
42
|
-
|
|
78
|
+
// musculoskeletal — malignant hyperthermia is a skeletal-muscle crisis and
|
|
79
|
+
// is matched here so the malignancy stem below never claims it
|
|
80
|
+
{ rx: /\b(arthralg|myalg|musculoskel|fracture|bone\s+pain|rhabdomyol|malignant\s+hyperthermi)/i, organ: "musculoskeletal" },
|
|
43
81
|
// immune
|
|
44
82
|
{ rx: /\b(hypersensitiv|anaphyl|autoimmune|cytokine\s+release)/i, organ: "immune" },
|
|
45
83
|
// reproductive
|
|
46
84
|
{ rx: /\b(menstrual|menorrhag|ovari|endometr|prostat|erectile)/i, organ: "reproductive" },
|
|
85
|
+
// oncology — last, so a malignancy that names its site is filed under that
|
|
86
|
+
// site and only a term whose content is the malignancy itself lands here
|
|
87
|
+
{ rx: /\b(neoplasm|malignan|carcinoma|sarcoma|lymphoma|leukaemi|leukemi|myeloma|myelodysplas|melanoma|metastas|tumour|tumor|cancer)/i, organ: "oncology" },
|
|
47
88
|
];
|
|
48
89
|
const UNINFORMATIVE_RE = /\b(incorrect\s+dose|off\s+label|drug\s+ineffective|product\s+(?:use|storage|administration)|accidental\s+exposure|inappropriate\s+schedule|exposure\s+via\s+skin|extra\s+dose|wrong\s+technique)\b/i;
|
|
49
90
|
export function classifyOrgan(meddraTerm) {
|
|
@@ -61,14 +102,18 @@ export function classifyOrgan(meddraTerm) {
|
|
|
61
102
|
* do not contain MedDRA PT stems, so `classifyOrgan` returns null for them —
|
|
62
103
|
* the SOC map is the primary path; `classifyOrgan(term)` is the fallback when
|
|
63
104
|
* the SOC string isn't in the map.
|
|
105
|
+
*
|
|
106
|
+
* A SOC that spans more than one canonical organ is absent: "Nervous system
|
|
107
|
+
* disorders" covers both nervous-system tokens and "Endocrine disorders" is
|
|
108
|
+
* broader than the thyroid, so a row under either resolves by its preferred
|
|
109
|
+
* term or not at all.
|
|
64
110
|
*/
|
|
65
111
|
export const TRIAL_AE_ORGAN_MAP = {
|
|
66
112
|
"Gastrointestinal disorders": "gi",
|
|
67
113
|
"Cardiac disorders": "cardiac",
|
|
68
|
-
"
|
|
114
|
+
"Vascular disorders": "vascular",
|
|
69
115
|
"Hepatobiliary disorders": "hepatic",
|
|
70
116
|
"Renal and urinary disorders": "renal",
|
|
71
|
-
"Endocrine disorders": "endocrine_thyroid",
|
|
72
117
|
"Respiratory, thoracic and mediastinal disorders": "respiratory",
|
|
73
118
|
"Skin and subcutaneous tissue disorders": "dermatologic",
|
|
74
119
|
"Musculoskeletal and connective tissue disorders": "musculoskeletal",
|
|
@@ -76,6 +121,8 @@ export const TRIAL_AE_ORGAN_MAP = {
|
|
|
76
121
|
"Blood and lymphatic system disorders": "hematologic",
|
|
77
122
|
"Reproductive system and breast disorders": "reproductive",
|
|
78
123
|
"Immune system disorders": "immune",
|
|
124
|
+
"Eye disorders": "ocular",
|
|
125
|
+
"Neoplasms benign, malignant and unspecified (incl cysts and polyps)": "oncology",
|
|
79
126
|
};
|
|
80
127
|
/**
|
|
81
128
|
* Classify a trial AE row by its SOC (preferred) or its individual MedDRA
|
|
@@ -89,38 +136,3 @@ export function classifyTrialAe(ae) {
|
|
|
89
136
|
return fromSoc;
|
|
90
137
|
return classifyOrgan(ae.term ?? "");
|
|
91
138
|
}
|
|
92
|
-
/**
|
|
93
|
-
* Classify a polypharm off-target panel row by its canonical `organ_system`
|
|
94
|
-
* string (set by the curated safety panel — `cardiac` | `hepatic` | `cns` |
|
|
95
|
-
* `renal` | `gi` | `hematologic` | `immune` | `metabolic` | `respiratory`).
|
|
96
|
-
*
|
|
97
|
-
* Falls back to `classifyOrgan` if the upstream string is non-canonical;
|
|
98
|
-
* this preserves coverage if a future collector emits MedDRA terms instead
|
|
99
|
-
* of canonical organ names.
|
|
100
|
-
*/
|
|
101
|
-
export function classifyPolypharmOrgan(organSystem) {
|
|
102
|
-
const sys = (organSystem ?? "").toLowerCase().trim();
|
|
103
|
-
if (!sys)
|
|
104
|
-
return null;
|
|
105
|
-
// Canonical names from the safety panel enum match by direct string equality.
|
|
106
|
-
if (CANONICAL_ORGAN_NAMES.has(sys))
|
|
107
|
-
return sys;
|
|
108
|
-
return classifyOrgan(sys);
|
|
109
|
-
}
|
|
110
|
-
const CANONICAL_ORGAN_NAMES = new Set([
|
|
111
|
-
"cardiac",
|
|
112
|
-
"hepatic",
|
|
113
|
-
"cns",
|
|
114
|
-
"renal",
|
|
115
|
-
"gi",
|
|
116
|
-
"pancreas",
|
|
117
|
-
"endocrine_thyroid",
|
|
118
|
-
"metabolic",
|
|
119
|
-
"hematologic",
|
|
120
|
-
"immune",
|
|
121
|
-
"respiratory",
|
|
122
|
-
"reproductive",
|
|
123
|
-
"dermatologic",
|
|
124
|
-
"musculoskeletal",
|
|
125
|
-
"oncology",
|
|
126
|
-
]);
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
const SCAFFOLD_TOKEN_RE = /per_section_synthesis\.[A-Za-z_][\w.]*(?:\[[\d–-]+\])?(?:\s+row\s+\d+)?/g;
|
|
15
15
|
// Citations to dossier.per_section_synthesis.* — this namespace doesn't exist
|
|
16
|
-
// in the
|
|
16
|
+
// in the dossier schema. The synthesis prompt occasionally emits these paths;
|
|
17
17
|
// strip them to prevent non-resolvable citations in recommendation prose.
|
|
18
18
|
const INVALID_PATH_RE = /\(?dossier\.per_section_synthesis\.[a-z_.[\]0-9]*\)?/gi;
|
|
19
19
|
// Dangling "dossier." with no path suffix is leftover templating; strip it.
|
|
@@ -5,14 +5,13 @@
|
|
|
5
5
|
* for each drug, then maps both into the dossier's RegulatoryActionRow
|
|
6
6
|
* shape.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* that previously lived next to it. Coverage that legitimately has no
|
|
8
|
+
* Coverage that legitimately has no
|
|
10
9
|
* machine-readable source (e.g., EMA assessment-report procedure codes like
|
|
11
10
|
* EMEA/H/A-31/1291) is omitted rather than backfilled with curation — the
|
|
12
11
|
* source_url points at the canonical document where the human reader can
|
|
13
12
|
* extract the additional context.
|
|
14
13
|
*/
|
|
15
|
-
import type {
|
|
14
|
+
import type { RegulatoryActionRow } from "@inflexa-ai/harness/contracts/target-dossier.js";
|
|
16
15
|
export interface DrugForRegulatoryActions {
|
|
17
16
|
chemblId: string;
|
|
18
17
|
name: string;
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* for each drug, then maps both into the dossier's RegulatoryActionRow
|
|
6
6
|
* shape.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* that previously lived next to it. Coverage that legitimately has no
|
|
8
|
+
* Coverage that legitimately has no
|
|
10
9
|
* machine-readable source (e.g., EMA assessment-report procedure codes like
|
|
11
10
|
* EMEA/H/A-31/1291) is omitted rather than backfilled with curation — the
|
|
12
11
|
* source_url points at the canonical document where the human reader can
|
|
@@ -15,6 +14,7 @@
|
|
|
15
14
|
import { withHost } from "../../../lib/host-concurrency.js";
|
|
16
15
|
import { getReferralsByDrug } from "../../../tools/lib/ema-client.js";
|
|
17
16
|
import { getDrugLabelActions } from "../../../tools/lib/openfda-client.js";
|
|
17
|
+
import { segmentLabelSafety } from "./fda-label-safety.js";
|
|
18
18
|
function formatEffectiveTime(yyyymmdd) {
|
|
19
19
|
if (!yyyymmdd || yyyymmdd.length < 6)
|
|
20
20
|
return "";
|
|
@@ -35,6 +35,29 @@ function clipFinding(text) {
|
|
|
35
35
|
return cleaned;
|
|
36
36
|
return `${cleaned.slice(0, FINDING_CHAR_LIMIT - 1).trimEnd()}…`;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Project a fetched label onto the shared typed safety form. Returns null when
|
|
40
|
+
* the label carries no application number: its warnings could not then be cited
|
|
41
|
+
* back to a document, and an uncitable warning is not evidence.
|
|
42
|
+
*/
|
|
43
|
+
function labelSafetyOf(drug, label) {
|
|
44
|
+
if (!label.applicationNumber)
|
|
45
|
+
return null;
|
|
46
|
+
const sections = [];
|
|
47
|
+
if (label.boxedWarning)
|
|
48
|
+
sections.push({ section: "boxed_warning", text: label.boxedWarning });
|
|
49
|
+
if (label.warningsAndCautions)
|
|
50
|
+
sections.push({ section: "warnings_and_precautions", text: label.warningsAndCautions });
|
|
51
|
+
if (sections.length === 0)
|
|
52
|
+
return null;
|
|
53
|
+
return {
|
|
54
|
+
application_number: label.applicationNumber,
|
|
55
|
+
drug_name: label.genericName ?? label.brandName ?? drug.name,
|
|
56
|
+
effective_time: label.effectiveTime ?? undefined,
|
|
57
|
+
source_url: label.sourceUrl || undefined,
|
|
58
|
+
sections,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
38
61
|
function fdaRowsFor(drug, labels) {
|
|
39
62
|
if (labels.length === 0)
|
|
40
63
|
return [];
|
|
@@ -42,6 +65,12 @@ function fdaRowsFor(drug, labels) {
|
|
|
42
65
|
const latest = sorted[0];
|
|
43
66
|
const rows = [];
|
|
44
67
|
const actionDate = formatEffectiveTime(latest.effectiveTime);
|
|
68
|
+
// The label's own prose, segmented once and attributed to organs, is what
|
|
69
|
+
// lets each row cite the section a reader can open rather than only the
|
|
70
|
+
// agency and the date.
|
|
71
|
+
const labelSafety = labelSafetyOf(drug, latest);
|
|
72
|
+
const signals = labelSafety ? segmentLabelSafety([labelSafety]).signals : [];
|
|
73
|
+
const evidenceFrom = (section) => signals.filter((s) => s.source_section === section).map((s) => s.evidence);
|
|
45
74
|
if (latest.boxedWarning) {
|
|
46
75
|
rows.push({
|
|
47
76
|
drug_chembl_id: drug.chemblId,
|
|
@@ -54,7 +83,7 @@ function fdaRowsFor(drug, labels) {
|
|
|
54
83
|
application_number: latest.applicationNumber ?? undefined,
|
|
55
84
|
finding: clipFinding(latest.boxedWarning),
|
|
56
85
|
source_url: latest.sourceUrl || undefined,
|
|
57
|
-
evidence:
|
|
86
|
+
evidence: evidenceFrom("boxed_warning"),
|
|
58
87
|
});
|
|
59
88
|
}
|
|
60
89
|
if (latest.warningsAndCautions) {
|
|
@@ -70,7 +99,7 @@ function fdaRowsFor(drug, labels) {
|
|
|
70
99
|
label_section: "5",
|
|
71
100
|
finding: clipFinding(latest.warningsAndCautions),
|
|
72
101
|
source_url: latest.sourceUrl || undefined,
|
|
73
|
-
evidence:
|
|
102
|
+
evidence: evidenceFrom("warnings_and_precautions"),
|
|
74
103
|
});
|
|
75
104
|
}
|
|
76
105
|
if (latest.hasRems) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The safety-evidence corroboration spine.
|
|
3
|
+
*
|
|
4
|
+
* A run queries a dozen-plus independent sources and files each one's output in
|
|
5
|
+
* its own dossier section. This fold asks the question none of those sections
|
|
6
|
+
* can: for a given organ, do the sources agree? Agreement across independent
|
|
7
|
+
* sources is itself evidence, and it is what separates a real organ liability
|
|
8
|
+
* from a single-source artefact.
|
|
9
|
+
*
|
|
10
|
+
* The fold re-fetches nothing. Every signal it reads is already in hand when it
|
|
11
|
+
* runs, and a source that also populates a section of its own is referenced
|
|
12
|
+
* here as a contribution rather than restated — there is one evidence path, not
|
|
13
|
+
* two.
|
|
14
|
+
*
|
|
15
|
+
* **The source list is open.** A source is one entry in `SIGNAL_SOURCES`
|
|
16
|
+
* declaring its id and how to read its signals out of the run's inputs.
|
|
17
|
+
* Everything downstream — grouping, counting, thresholding, evidence assembly —
|
|
18
|
+
* is written against the id-bearing record, so a fifteenth collector joins the
|
|
19
|
+
* spine by adding an entry and changes neither the fold nor the schema.
|
|
20
|
+
*/
|
|
21
|
+
import type { SafetyCorroboration } from "@inflexa-ai/harness/contracts/target-dossier.js";
|
|
22
|
+
import type { Phase1Bundle } from "../schemas.js";
|
|
23
|
+
import type { OrganSignalProjection } from "./fda-label-safety.js";
|
|
24
|
+
/** Distinct sources an organ needs before the spine calls it corroborated. */
|
|
25
|
+
export declare const MIN_INDEPENDENT_SOURCES = 2;
|
|
26
|
+
/** What the section reports as having emptied it. */
|
|
27
|
+
export declare const CORROBORATION_FILTER: string;
|
|
28
|
+
/** Everything the fold reads. Already collected — nothing here is fetched. */
|
|
29
|
+
export interface CorroborationInput {
|
|
30
|
+
readonly phase1: Phase1Bundle;
|
|
31
|
+
/** Per-organ FDA label warnings segmented at the approval-precedent step. */
|
|
32
|
+
readonly regulatoryOrganSignals: OrganSignalProjection | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Fold the run's per-organ safety signals into one corroboration record per
|
|
36
|
+
* organ.
|
|
37
|
+
*
|
|
38
|
+
* `dropped_count` counts signals — the fold's input unit — discarded because
|
|
39
|
+
* their organ did not resolve, because their source produced no locator, or
|
|
40
|
+
* because their organ drew fewer than `MIN_INDEPENDENT_SOURCES` distinct
|
|
41
|
+
* sources. A fold that ends with no record reports `filtered` with that count
|
|
42
|
+
* rather than an empty `available`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function assembleSafetyCorroboration(input: CorroborationInput): SafetyCorroboration;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The safety-evidence corroboration spine.
|
|
3
|
+
*
|
|
4
|
+
* A run queries a dozen-plus independent sources and files each one's output in
|
|
5
|
+
* its own dossier section. This fold asks the question none of those sections
|
|
6
|
+
* can: for a given organ, do the sources agree? Agreement across independent
|
|
7
|
+
* sources is itself evidence, and it is what separates a real organ liability
|
|
8
|
+
* from a single-source artefact.
|
|
9
|
+
*
|
|
10
|
+
* The fold re-fetches nothing. Every signal it reads is already in hand when it
|
|
11
|
+
* runs, and a source that also populates a section of its own is referenced
|
|
12
|
+
* here as a contribution rather than restated — there is one evidence path, not
|
|
13
|
+
* two.
|
|
14
|
+
*
|
|
15
|
+
* **The source list is open.** A source is one entry in `SIGNAL_SOURCES`
|
|
16
|
+
* declaring its id and how to read its signals out of the run's inputs.
|
|
17
|
+
* Everything downstream — grouping, counting, thresholding, evidence assembly —
|
|
18
|
+
* is written against the id-bearing record, so a fifteenth collector joins the
|
|
19
|
+
* spine by adding an entry and changes neither the fold nor the schema.
|
|
20
|
+
*/
|
|
21
|
+
import { isSafetyRelevant, meetsTpmFloor, resolveTissueOrganSystem } from "../assemblers/safety.js";
|
|
22
|
+
import { resolveHpoOrgan } from "./hpo-organ-map.js";
|
|
23
|
+
import { resolveImpcOrgan } from "./impc-organ-map.js";
|
|
24
|
+
import { classifyOrgan } from "./meddra-organ-map.js";
|
|
25
|
+
/** Distinct sources an organ needs before the spine calls it corroborated. */
|
|
26
|
+
export const MIN_INDEPENDENT_SOURCES = 2;
|
|
27
|
+
/** What the section reports as having emptied it. */
|
|
28
|
+
export const CORROBORATION_FILTER = `organ signals with no resolvable organ or locator, or whose organ drew fewer than ` + `${MIN_INDEPENDENT_SOURCES} independent sources`;
|
|
29
|
+
/**
|
|
30
|
+
* Curated human phenotypes ranked per organ. A well-annotated gene carries
|
|
31
|
+
* hundreds of HPO terms under one organ system; beyond the ranked head they
|
|
32
|
+
* restate the same organ-level signal, so the source raises it once at the
|
|
33
|
+
* granularity a reader can act on.
|
|
34
|
+
*/
|
|
35
|
+
const PHENOTYPES_PER_ORGAN = 3;
|
|
36
|
+
// ── Source extractors ────────────────────────────────────────────────
|
|
37
|
+
/** Warning fragments from FDA prescribing information, already organ-keyed. */
|
|
38
|
+
function extractRegulatoryLabels(input) {
|
|
39
|
+
const projection = input.regulatoryOrganSignals;
|
|
40
|
+
if (!projection)
|
|
41
|
+
return [];
|
|
42
|
+
return projection.signals.map((s) => ({
|
|
43
|
+
organ: s.organ,
|
|
44
|
+
signal: `${s.drug_name} label §${s.label_section}: ${s.excerpt}`,
|
|
45
|
+
evidence: s.evidence.regulatory_reference ? s.evidence : null,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
/** Mouse knockout phenotype systems, keyed by the MGI accession for the gene. */
|
|
49
|
+
function extractKnockoutPhenotypes(input) {
|
|
50
|
+
const impc = input.phase1.collectors.impc;
|
|
51
|
+
if (impc.coverage !== "available")
|
|
52
|
+
return [];
|
|
53
|
+
const accession = impc.data.mgiAccessionId;
|
|
54
|
+
return impc.data.organSystems.map((bucket) => ({
|
|
55
|
+
organ: resolveImpcOrgan(bucket),
|
|
56
|
+
signal: `mouse knockout phenotype in the ${bucket} system`,
|
|
57
|
+
evidence: accession
|
|
58
|
+
? {
|
|
59
|
+
source: "impc",
|
|
60
|
+
accession,
|
|
61
|
+
predicate: "knockout_phenotype",
|
|
62
|
+
excerpt: `${impc.data.mouseMarkerSymbol ?? ""} knockout phenotypes in the ${bucket} system`.trim(),
|
|
63
|
+
metadata: { bucket, phenotype_count: impc.data.phenotypeCount },
|
|
64
|
+
}
|
|
65
|
+
: null,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
/** Curated human phenotypes, resolved by HPO ancestry rather than by label. */
|
|
69
|
+
function extractHumanPhenotypes(input) {
|
|
70
|
+
const monarch = input.phase1.collectors.monarch;
|
|
71
|
+
if (monarch.coverage !== "available")
|
|
72
|
+
return [];
|
|
73
|
+
const ranked = [...monarch.data.phenotypes].sort((a, b) => b.publications.length - a.publications.length || (b.frequencyPercent ?? 0) - (a.frequencyPercent ?? 0) || a.label.localeCompare(b.label));
|
|
74
|
+
const perOrgan = new Map();
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const p of ranked) {
|
|
77
|
+
const organ = resolveHpoOrgan(p.hpoId, p.ancestorIds);
|
|
78
|
+
if (organ === null) {
|
|
79
|
+
out.push({ organ: null, signal: p.label, evidence: null });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const seen = perOrgan.get(organ) ?? 0;
|
|
83
|
+
if (seen >= PHENOTYPES_PER_ORGAN)
|
|
84
|
+
continue;
|
|
85
|
+
perOrgan.set(organ, seen + 1);
|
|
86
|
+
const pmid = p.publications.find((pub) => pub.startsWith("PMID:"))?.slice("PMID:".length);
|
|
87
|
+
out.push({
|
|
88
|
+
organ,
|
|
89
|
+
signal: `curated human phenotype: ${p.label}`,
|
|
90
|
+
evidence: {
|
|
91
|
+
source: "monarch",
|
|
92
|
+
accession: p.hpoId,
|
|
93
|
+
...(pmid ? { pmid } : {}),
|
|
94
|
+
predicate: "human_phenotype",
|
|
95
|
+
is_human: true,
|
|
96
|
+
excerpt: p.label,
|
|
97
|
+
metadata: {
|
|
98
|
+
...(p.diseaseContext ? { disease_context: p.diseaseContext } : {}),
|
|
99
|
+
...(p.frequencyPercent != null ? { frequency_percent: p.frequencyPercent } : {}),
|
|
100
|
+
...(p.primaryKnowledgeSource ? { knowledge_source: p.primaryKnowledgeSource } : {}),
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/** Open Targets' curated safety liabilities, keyed by the Ensembl accession. */
|
|
108
|
+
function extractCuratedSafetyLiabilities(input) {
|
|
109
|
+
const ot = input.phase1.collectors.opentargets;
|
|
110
|
+
if (ot.coverage !== "available")
|
|
111
|
+
return [];
|
|
112
|
+
return ot.data.safetyLiabilities.map((liability) => ({
|
|
113
|
+
organ: classifyOrgan(liability.event) ?? classifyOrgan(liability.biosamples.join(" ")),
|
|
114
|
+
signal: `curated safety liability: ${liability.event}`,
|
|
115
|
+
evidence: {
|
|
116
|
+
source: "opentargets:safety",
|
|
117
|
+
accession: ot.data.ensemblId,
|
|
118
|
+
predicate: "known_safety_liability",
|
|
119
|
+
excerpt: liability.effects ? `${liability.event} — ${liability.effects}` : liability.event,
|
|
120
|
+
metadata: { biosamples: liability.biosamples, curator: liability.source },
|
|
121
|
+
},
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Baseline expression in safety-relevant tissues.
|
|
126
|
+
*
|
|
127
|
+
* The atlas reaches the dossier through Open Targets' consensus expression,
|
|
128
|
+
* which is Human Protein Atlas data — the same path
|
|
129
|
+
* `reference_biology.normal_tissue_expression` and the off-tissue risk rows
|
|
130
|
+
* read, with the same safety-relevance and floor rules applied. Only the
|
|
131
|
+
* highest-expressing tissue per organ is raised: several tissues mapping to one
|
|
132
|
+
* organ are one expression signal about that organ.
|
|
133
|
+
*/
|
|
134
|
+
function extractExpressionAtlas(input) {
|
|
135
|
+
const expression = input.phase1.collectors.expressionHuman;
|
|
136
|
+
if (expression.coverage !== "available")
|
|
137
|
+
return [];
|
|
138
|
+
const accession = input.phase1.resolved.ids.ensembl;
|
|
139
|
+
const best = new Map();
|
|
140
|
+
const unresolved = [];
|
|
141
|
+
for (const t of expression.data.tissues) {
|
|
142
|
+
const value = t.value ?? 0;
|
|
143
|
+
if (!isSafetyRelevant(t.tissueLabel, t.organSystem) || !meetsTpmFloor(t.tissueLabel, value))
|
|
144
|
+
continue;
|
|
145
|
+
const organ = resolveTissueOrganSystem(t.tissueLabel, t.organSystem);
|
|
146
|
+
if (organ === null) {
|
|
147
|
+
unresolved.push({ organ: null, signal: `${t.tissueLabel} expression`, evidence: null });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const current = best.get(organ);
|
|
151
|
+
if (!current || value > current.value)
|
|
152
|
+
best.set(organ, { tissue: t.tissueLabel, value });
|
|
153
|
+
}
|
|
154
|
+
const resolved = [...best.entries()].map(([organ, hit]) => {
|
|
155
|
+
const signal = `${expression.data.unit} expression ${hit.value} in ${hit.tissue}`;
|
|
156
|
+
return {
|
|
157
|
+
organ,
|
|
158
|
+
signal,
|
|
159
|
+
evidence: accession
|
|
160
|
+
? {
|
|
161
|
+
source: `expression:${expression.data.source}`,
|
|
162
|
+
accession,
|
|
163
|
+
predicate: "expressed_in_safety_tissue",
|
|
164
|
+
score: hit.value,
|
|
165
|
+
excerpt: `${hit.tissue}: ${hit.value} (${expression.data.unit})`,
|
|
166
|
+
metadata: {
|
|
167
|
+
tissue: hit.tissue,
|
|
168
|
+
unit: expression.data.unit,
|
|
169
|
+
normalization_notes: expression.data.normalization_notes,
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
: null,
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
return [...resolved, ...unresolved];
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The registered sources.
|
|
179
|
+
*
|
|
180
|
+
* This array is the whole integration surface: an id, and how to read that
|
|
181
|
+
* source's organ-bearing signals out of what the run already collected.
|
|
182
|
+
*/
|
|
183
|
+
const SIGNAL_SOURCES = [
|
|
184
|
+
{ id: "fda_label", extract: extractRegulatoryLabels },
|
|
185
|
+
{ id: "impc", extract: extractKnockoutPhenotypes },
|
|
186
|
+
{ id: "monarch", extract: extractHumanPhenotypes },
|
|
187
|
+
{ id: "opentargets_safety", extract: extractCuratedSafetyLiabilities },
|
|
188
|
+
{ id: "expression_atlas", extract: extractExpressionAtlas },
|
|
189
|
+
];
|
|
190
|
+
/**
|
|
191
|
+
* Fold the run's per-organ safety signals into one corroboration record per
|
|
192
|
+
* organ.
|
|
193
|
+
*
|
|
194
|
+
* `dropped_count` counts signals — the fold's input unit — discarded because
|
|
195
|
+
* their organ did not resolve, because their source produced no locator, or
|
|
196
|
+
* because their organ drew fewer than `MIN_INDEPENDENT_SOURCES` distinct
|
|
197
|
+
* sources. A fold that ends with no record reports `filtered` with that count
|
|
198
|
+
* rather than an empty `available`.
|
|
199
|
+
*/
|
|
200
|
+
export function assembleSafetyCorroboration(input) {
|
|
201
|
+
const sourcesConsidered = [];
|
|
202
|
+
const admitted = [];
|
|
203
|
+
let dropped = 0;
|
|
204
|
+
for (const source of SIGNAL_SOURCES) {
|
|
205
|
+
const signals = source.extract(input);
|
|
206
|
+
if (signals.length === 0)
|
|
207
|
+
continue;
|
|
208
|
+
sourcesConsidered.push(source.id);
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
for (const raw of signals) {
|
|
211
|
+
if (raw.organ === null || raw.evidence === null) {
|
|
212
|
+
dropped += 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const key = `${raw.organ}\u0000${raw.signal}`;
|
|
216
|
+
if (seen.has(key))
|
|
217
|
+
continue;
|
|
218
|
+
seen.add(key);
|
|
219
|
+
admitted.push({
|
|
220
|
+
source: source.id,
|
|
221
|
+
organ: raw.organ,
|
|
222
|
+
contribution: { source: source.id, signal: raw.signal, evidence: raw.evidence },
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (sourcesConsidered.length === 0) {
|
|
227
|
+
return {
|
|
228
|
+
coverage: "queried_no_data",
|
|
229
|
+
error: { message: "no source produced an organ-bearing safety signal" },
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const byOrgan = new Map();
|
|
233
|
+
for (const item of admitted) {
|
|
234
|
+
const bucket = byOrgan.get(item.organ);
|
|
235
|
+
if (bucket)
|
|
236
|
+
bucket.push(item);
|
|
237
|
+
else
|
|
238
|
+
byOrgan.set(item.organ, [item]);
|
|
239
|
+
}
|
|
240
|
+
const rows = [];
|
|
241
|
+
for (const [organ, items] of byOrgan) {
|
|
242
|
+
const corroborating = [...new Set(items.map((i) => i.source))].sort();
|
|
243
|
+
if (corroborating.length < MIN_INDEPENDENT_SOURCES) {
|
|
244
|
+
dropped += items.length;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const contributions = items.map((i) => i.contribution);
|
|
248
|
+
rows.push({
|
|
249
|
+
organ,
|
|
250
|
+
contributions,
|
|
251
|
+
corroborating_sources: corroborating,
|
|
252
|
+
independent_source_count: corroborating.length,
|
|
253
|
+
support: { state: "scored", evidence: contributions.map((c) => c.evidence) },
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
rows.sort((a, b) => b.independent_source_count - a.independent_source_count || a.organ.localeCompare(b.organ));
|
|
257
|
+
// Reaching here means at least one source emitted a signal — that is what put
|
|
258
|
+
// it in `sourcesConsidered` — and every emitted signal was either admitted or
|
|
259
|
+
// counted as dropped. So no record left standing means the fold's own
|
|
260
|
+
// thresholds took them all, and `dropped` is necessarily positive.
|
|
261
|
+
if (rows.length === 0) {
|
|
262
|
+
return { coverage: "filtered", filter: CORROBORATION_FILTER, dropped_count: dropped };
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
coverage: "available",
|
|
266
|
+
data: {
|
|
267
|
+
rows,
|
|
268
|
+
sources_considered: [...sourcesConsidered].sort(),
|
|
269
|
+
min_independent_sources: MIN_INDEPENDENT_SOURCES,
|
|
270
|
+
},
|
|
271
|
+
...(dropped > 0 ? { dropped_count: dropped } : {}),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Trial-attribution classifier + UniProt-backed identity resolvers.
|
|
3
3
|
*
|
|
4
|
-
* `resolveOnTargetChemblIds` and `resolveFamilySiblingUniprots`
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* for receptor-family siblings) so no per-target editing is needed when a
|
|
9
|
-
* new HGNC target is assessed.
|
|
4
|
+
* `resolveOnTargetChemblIds` and `resolveFamilySiblingUniprots` fetch from
|
|
5
|
+
* authoritative sources at assembly time — UniProt for ChEMBL cross-references,
|
|
6
|
+
* IUPHAR Guide to Pharmacology for receptor-family siblings — so assessing a
|
|
7
|
+
* new HGNC target needs no per-target curation here.
|
|
10
8
|
*
|
|
11
9
|
* The actual loop-level checks (`isOnTargetChemblId` etc.) take the
|
|
12
10
|
* resolved arrays as plain inputs so the per-row assembly path stays
|
|
@@ -48,7 +46,7 @@ export declare function resolveOnTargetChemblIds(assessmentUniprot: string): Pro
|
|
|
48
46
|
* Resolve UniProt accessions of sibling receptors in the same IUPHAR family
|
|
49
47
|
* as the assessment target. Used by `classifyTrialAttribution` so a trial
|
|
50
48
|
* whose intervention hits a paralog (e.g., CALCRL when CALCR is the
|
|
51
|
-
* assessment target) lands in
|
|
49
|
+
* assessment target) lands in the related bucket rather than the
|
|
52
50
|
* primary trials list.
|
|
53
51
|
*/
|
|
54
52
|
export declare function resolveFamilySiblingUniprots(uniprotOrGeneSymbol: string): Promise<string[]>;
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Trial-attribution classifier + UniProt-backed identity resolvers.
|
|
3
3
|
*
|
|
4
|
-
* `resolveOnTargetChemblIds` and `resolveFamilySiblingUniprots`
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* for receptor-family siblings) so no per-target editing is needed when a
|
|
9
|
-
* new HGNC target is assessed.
|
|
4
|
+
* `resolveOnTargetChemblIds` and `resolveFamilySiblingUniprots` fetch from
|
|
5
|
+
* authoritative sources at assembly time — UniProt for ChEMBL cross-references,
|
|
6
|
+
* IUPHAR Guide to Pharmacology for receptor-family siblings — so assessing a
|
|
7
|
+
* new HGNC target needs no per-target curation here.
|
|
10
8
|
*
|
|
11
9
|
* The actual loop-level checks (`isOnTargetChemblId` etc.) take the
|
|
12
10
|
* resolved arrays as plain inputs so the per-row assembly path stays
|
|
@@ -63,7 +61,7 @@ export async function resolveOnTargetChemblIds(assessmentUniprot) {
|
|
|
63
61
|
* Resolve UniProt accessions of sibling receptors in the same IUPHAR family
|
|
64
62
|
* as the assessment target. Used by `classifyTrialAttribution` so a trial
|
|
65
63
|
* whose intervention hits a paralog (e.g., CALCRL when CALCR is the
|
|
66
|
-
* assessment target) lands in
|
|
64
|
+
* assessment target) lands in the related bucket rather than the
|
|
67
65
|
* primary trials list.
|
|
68
66
|
*/
|
|
69
67
|
export async function resolveFamilySiblingUniprots(uniprotOrGeneSymbol) {
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenTelemetry counters for the target-assessment DBOS workflow.
|
|
3
3
|
*
|
|
4
|
-
* Surfaces
|
|
4
|
+
* Surfaces two signals:
|
|
5
5
|
* - `cortex.harness.ta.workflow.terminal_reason` — per terminal path; the
|
|
6
6
|
* alert in §16.5/§16.6 reads this. `reason` ∈ {completed,
|
|
7
7
|
* target-unresolved, schema-violation, derived-invariant-violation,
|
|
8
8
|
* unexpected-throw, suspended-on-402, operator-cancelled}.
|
|
9
|
-
* - `cortex.harness.ta.llm.attempts` — histogram per LLM step (caller
|
|
10
|
-
* samples on entry to detect probe-retry loops).
|
|
11
9
|
* - billing cache_hit/_miss live in `harness/billing/target-assessment-resolver.ts`.
|
|
12
10
|
*
|
|
13
11
|
* Memoised meter so a re-imported module in tests doesn't recreate the
|
|
@@ -15,7 +13,4 @@
|
|
|
15
13
|
* "duplicate instrument" warnings).
|
|
16
14
|
*/
|
|
17
15
|
export type TaTerminalReason = "completed" | "target-unresolved" | "schema-violation" | "derived-invariant-violation" | "unexpected-throw" | "suspended-on-402" | "operator-cancelled" | "deleted";
|
|
18
|
-
/** Test hook — drop memoised instruments to rebind a fresh MeterProvider. */
|
|
19
|
-
export declare function __resetTaWorkflowMetricsForTest(): void;
|
|
20
16
|
export declare function recordTerminalReason(reason: TaTerminalReason): void;
|
|
21
|
-
export declare function recordLlmAttempt(agentId: string, attempt: number): void;
|