@inflexa-ai/harness 0.35.8 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/dist/auth/types.d.ts +5 -14
  2. package/dist/auth/types.js +3 -10
  3. package/dist/billing/usage-recorder.d.ts +1 -1
  4. package/dist/contracts/analogy-report.d.ts +1 -1
  5. package/dist/contracts/analogy-report.js +1 -1
  6. package/dist/contracts/chat-parts.d.ts +2 -3
  7. package/dist/contracts/index.d.ts +0 -4
  8. package/dist/contracts/index.js +0 -2
  9. package/dist/index.d.ts +0 -7
  10. package/dist/index.js +0 -21
  11. package/dist/lib/metrics.d.ts +1 -1
  12. package/dist/lib/otel-spans.d.ts +0 -2
  13. package/dist/lib/otel-spans.js +0 -2
  14. package/dist/loop/run-agent.d.ts +1 -1
  15. package/dist/runtime/assemble.d.ts +0 -3
  16. package/dist/runtime/assemble.js +0 -3
  17. package/dist/state/index.d.ts +0 -2
  18. package/dist/state/index.js +0 -1
  19. package/dist/state/init.js +1 -60
  20. package/dist/state/purge-analysis.d.ts +1 -2
  21. package/dist/state/purge-analysis.js +1 -2
  22. package/dist/tasks/regulatory-corpus-boot-refresh.js +1 -1
  23. package/dist/tools/bio/gene-preclinical-profile.d.ts +1 -3
  24. package/dist/tools/bio/gene-preclinical-profile.js +1 -3
  25. package/dist/tools/lib/chembl-client.d.ts +1 -4
  26. package/dist/tools/lib/chembl-client.js +1 -4
  27. package/dist/tools/lib/clinical-benchmarks-client.d.ts +1 -1
  28. package/dist/tools/lib/clinical-benchmarks-client.js +1 -1
  29. package/dist/tools/lib/clinical-trials-client.d.ts +0 -3
  30. package/dist/tools/lib/clinical-trials-client.js +0 -3
  31. package/dist/tools/lib/identifier-resolver.d.ts +5 -7
  32. package/dist/tools/lib/identifier-resolver.js +5 -7
  33. package/dist/tools/lib/iuphar-client.d.ts +1 -3
  34. package/dist/tools/lib/iuphar-client.js +1 -3
  35. package/dist/tools/lib/opentargets-client.d.ts +1 -1
  36. package/dist/tools/lib/opentargets-client.js +1 -1
  37. package/dist/tools/lib/protein-family-normalize.d.ts +5 -7
  38. package/dist/tools/lib/protein-family-normalize.js +5 -7
  39. package/dist/tools/lib/uniprot-client.d.ts +0 -7
  40. package/dist/tools/lib/uniprot-client.js +2 -9
  41. package/package.json +1 -1
  42. package/dist/contracts/target-assessment-row.d.ts +0 -85
  43. package/dist/contracts/target-assessment-row.js +0 -37
  44. package/dist/contracts/target-dossier.d.ts +0 -12423
  45. package/dist/contracts/target-dossier.js +0 -1282
  46. package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +0 -1
  47. package/dist/prompts/target-assessment/briefs/executive-recommendation.js +0 -85
  48. package/dist/prompts/target-assessment/briefs/index.d.ts +0 -4
  49. package/dist/prompts/target-assessment/briefs/index.js +0 -4
  50. package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +0 -1
  51. package/dist/prompts/target-assessment/briefs/liability-bullets.js +0 -114
  52. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +0 -1
  53. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +0 -37
  54. package/dist/prompts/target-assessment/briefs/translational-commentary.d.ts +0 -1
  55. package/dist/prompts/target-assessment/briefs/translational-commentary.js +0 -23
  56. package/dist/prompts/target-assessment/drugs-in-class.d.ts +0 -1
  57. package/dist/prompts/target-assessment/drugs-in-class.js +0 -29
  58. package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +0 -1
  59. package/dist/prompts/target-assessment/investigation/adversarial-critique.js +0 -67
  60. package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +0 -1
  61. package/dist/prompts/target-assessment/investigation/claim-reverification.js +0 -49
  62. package/dist/prompts/target-assessment/investigation/index.d.ts +0 -3
  63. package/dist/prompts/target-assessment/investigation/index.js +0 -3
  64. package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +0 -1
  65. package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +0 -49
  66. package/dist/prompts/target-assessment/modulator-triage.d.ts +0 -1
  67. package/dist/prompts/target-assessment/modulator-triage.js +0 -31
  68. package/dist/prompts/target-assessment/off-target-annotator.d.ts +0 -1
  69. package/dist/prompts/target-assessment/off-target-annotator.js +0 -28
  70. package/dist/prompts/target-assessment/tox-voice/core.d.ts +0 -1
  71. package/dist/prompts/target-assessment/tox-voice/core.js +0 -74
  72. package/dist/prompts/target-assessment/tox-voice/critique.d.ts +0 -7
  73. package/dist/prompts/target-assessment/tox-voice/critique.js +0 -109
  74. package/dist/prompts/target-assessment/tox-voice/exemplars.d.ts +0 -8
  75. package/dist/prompts/target-assessment/tox-voice/exemplars.js +0 -24
  76. package/dist/prompts/target-assessment/tox-voice/index.d.ts +0 -4
  77. package/dist/prompts/target-assessment/tox-voice/index.js +0 -4
  78. package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +0 -18
  79. package/dist/prompts/target-assessment/tox-voice/vocabulary.js +0 -61
  80. package/dist/state/target-assessments.d.ts +0 -68
  81. package/dist/state/target-assessments.js +0 -222
  82. package/dist/workflows/execute-target-assessment.d.ts +0 -118
  83. package/dist/workflows/execute-target-assessment.js +0 -631
  84. package/dist/workflows/target-assessment/assemblers/index.d.ts +0 -5
  85. package/dist/workflows/target-assessment/assemblers/index.js +0 -4
  86. package/dist/workflows/target-assessment/assemblers/literature.d.ts +0 -281
  87. package/dist/workflows/target-assessment/assemblers/literature.js +0 -971
  88. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +0 -118
  89. package/dist/workflows/target-assessment/assemblers/orchestrator.js +0 -945
  90. package/dist/workflows/target-assessment/assemblers/safety.d.ts +0 -374
  91. package/dist/workflows/target-assessment/assemblers/safety.js +0 -1068
  92. package/dist/workflows/target-assessment/assemblers/trials.d.ts +0 -30
  93. package/dist/workflows/target-assessment/assemblers/trials.js +0 -75
  94. package/dist/workflows/target-assessment/collectors/index.d.ts +0 -114
  95. package/dist/workflows/target-assessment/collectors/index.js +0 -538
  96. package/dist/workflows/target-assessment/coverage.d.ts +0 -97
  97. package/dist/workflows/target-assessment/coverage.js +0 -86
  98. package/dist/workflows/target-assessment/decisions/index.d.ts +0 -86
  99. package/dist/workflows/target-assessment/decisions/index.js +0 -127
  100. package/dist/workflows/target-assessment/fanout/index.d.ts +0 -32
  101. package/dist/workflows/target-assessment/fanout/index.js +0 -156
  102. package/dist/workflows/target-assessment/investigation/index.d.ts +0 -97
  103. package/dist/workflows/target-assessment/investigation/index.js +0 -419
  104. package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +0 -73
  105. package/dist/workflows/target-assessment/lib/approval-precedents.js +0 -185
  106. package/dist/workflows/target-assessment/lib/citation-validator.d.ts +0 -46
  107. package/dist/workflows/target-assessment/lib/citation-validator.js +0 -316
  108. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +0 -65
  109. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.js +0 -174
  110. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +0 -44
  111. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -152
  112. package/dist/workflows/target-assessment/lib/compute-derived.d.ts +0 -20
  113. package/dist/workflows/target-assessment/lib/compute-derived.js +0 -261
  114. package/dist/workflows/target-assessment/lib/compute-selectivity.d.ts +0 -15
  115. package/dist/workflows/target-assessment/lib/compute-selectivity.js +0 -18
  116. package/dist/workflows/target-assessment/lib/dedup-modulators.d.ts +0 -37
  117. package/dist/workflows/target-assessment/lib/dedup-modulators.js +0 -157
  118. package/dist/workflows/target-assessment/lib/evidence-conflict-detector.d.ts +0 -17
  119. package/dist/workflows/target-assessment/lib/evidence-conflict-detector.js +0 -100
  120. package/dist/workflows/target-assessment/lib/expression-constants.d.ts +0 -21
  121. package/dist/workflows/target-assessment/lib/expression-constants.js +0 -21
  122. package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +0 -20
  123. package/dist/workflows/target-assessment/lib/family-complex-supplement.js +0 -36
  124. package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +0 -72
  125. package/dist/workflows/target-assessment/lib/fda-label-safety.js +0 -166
  126. package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -30
  127. package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -44
  128. package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +0 -34
  129. package/dist/workflows/target-assessment/lib/hpo-organ-map.js +0 -63
  130. package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +0 -19
  131. package/dist/workflows/target-assessment/lib/impc-organ-map.js +0 -39
  132. package/dist/workflows/target-assessment/lib/intended-polypharm-filter.d.ts +0 -4
  133. package/dist/workflows/target-assessment/lib/intended-polypharm-filter.js +0 -24
  134. package/dist/workflows/target-assessment/lib/llm-step.d.ts +0 -80
  135. package/dist/workflows/target-assessment/lib/llm-step.js +0 -101
  136. package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +0 -40
  137. package/dist/workflows/target-assessment/lib/meddra-organ-map.js +0 -138
  138. package/dist/workflows/target-assessment/lib/recommendation-quality-gates.d.ts +0 -17
  139. package/dist/workflows/target-assessment/lib/recommendation-quality-gates.js +0 -99
  140. package/dist/workflows/target-assessment/lib/recommendation-sanitizer.d.ts +0 -2
  141. package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +0 -51
  142. package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +0 -25
  143. package/dist/workflows/target-assessment/lib/regulatory-actions.js +0 -176
  144. package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +0 -44
  145. package/dist/workflows/target-assessment/lib/safety-corroboration.js +0 -273
  146. package/dist/workflows/target-assessment/lib/structured-llm.d.ts +0 -61
  147. package/dist/workflows/target-assessment/lib/structured-llm.js +0 -102
  148. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +0 -59
  149. package/dist/workflows/target-assessment/lib/target-identity-filter.js +0 -95
  150. package/dist/workflows/target-assessment/lib/therapeutic-programs.d.ts +0 -20
  151. package/dist/workflows/target-assessment/lib/therapeutic-programs.js +0 -68
  152. package/dist/workflows/target-assessment/metrics.d.ts +0 -16
  153. package/dist/workflows/target-assessment/metrics.js +0 -30
  154. package/dist/workflows/target-assessment/phase4-assemble.d.ts +0 -3182
  155. package/dist/workflows/target-assessment/phase4-assemble.js +0 -39
  156. package/dist/workflows/target-assessment/phase5-persist.d.ts +0 -59
  157. package/dist/workflows/target-assessment/phase5-persist.js +0 -349
  158. package/dist/workflows/target-assessment/progress-stream.d.ts +0 -75
  159. package/dist/workflows/target-assessment/progress-stream.js +0 -110
  160. package/dist/workflows/target-assessment/progress.d.ts +0 -39
  161. package/dist/workflows/target-assessment/progress.js +0 -77
  162. package/dist/workflows/target-assessment/schemas.d.ts +0 -970
  163. package/dist/workflows/target-assessment/schemas.js +0 -318
  164. package/dist/workflows/target-assessment/steps/collectors/pathways.d.ts +0 -15
  165. package/dist/workflows/target-assessment/steps/collectors/pathways.js +0 -58
  166. package/dist/workflows/target-assessment/steps/fanout/aes-for-one-class-drug-step.d.ts +0 -34
  167. package/dist/workflows/target-assessment/steps/fanout/aes-for-one-class-drug-step.js +0 -31
  168. package/dist/workflows/target-assessment/steps/fanout/aes-for-one-trial-step.d.ts +0 -57
  169. package/dist/workflows/target-assessment/steps/fanout/aes-for-one-trial-step.js +0 -54
  170. package/dist/workflows/target-assessment/steps/fanout/faers-for-one-modulator-step.d.ts +0 -35
  171. package/dist/workflows/target-assessment/steps/fanout/faers-for-one-modulator-step.js +0 -32
  172. package/dist/workflows/target-assessment/steps/fanout/index.d.ts +0 -14
  173. package/dist/workflows/target-assessment/steps/fanout/index.js +0 -10
  174. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +0 -29
  175. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +0 -29
  176. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +0 -657
  177. package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -34
  178. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +0 -806
  179. package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -16
  180. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -28
  181. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -27
  182. package/dist/workflows/target-assessment/sub-workflows/index.d.ts +0 -15
  183. package/dist/workflows/target-assessment/sub-workflows/index.js +0 -11
  184. package/dist/workflows/target-assessment/sub-workflows/per-class-drug-aes.d.ts +0 -41
  185. package/dist/workflows/target-assessment/sub-workflows/per-class-drug-aes.js +0 -12
  186. package/dist/workflows/target-assessment/sub-workflows/per-modulator-faers.d.ts +0 -41
  187. package/dist/workflows/target-assessment/sub-workflows/per-modulator-faers.js +0 -12
  188. package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +0 -37
  189. package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.js +0 -12
  190. package/dist/workflows/target-assessment/sub-workflows/per-trial-aes.d.ts +0 -66
  191. package/dist/workflows/target-assessment/sub-workflows/per-trial-aes.js +0 -12
  192. package/dist/workflows/target-assessment/synthesis/index.d.ts +0 -607
  193. package/dist/workflows/target-assessment/synthesis/index.js +0 -354
  194. package/dist/workflows/target-assessment/synthesis-probes.d.ts +0 -56
  195. package/dist/workflows/target-assessment/synthesis-probes.js +0 -320
@@ -1,46 +0,0 @@
1
- export type RecommendationAuditEntry = {
2
- surface: "rationale" | "key_strengths" | "key_risks" | "modality_choice";
3
- path: string;
4
- excerpt: string;
5
- } | {
6
- surface: "external_missing";
7
- id: string;
8
- excerpt: string;
9
- } | {
10
- surface: "organ_claim_without_probe_pass";
11
- excerpt: string;
12
- bullet_category: string;
13
- } | {
14
- surface: "nct_wrong_class";
15
- id: string;
16
- excerpt: string;
17
- } | {
18
- surface: "pmid_not_in_key_papers";
19
- id: string;
20
- excerpt: string;
21
- };
22
- export type RecommendationAudit = {
23
- citations_total: number;
24
- citations_unresolved: RecommendationAuditEntry[];
25
- non_dossier_citations: Array<{
26
- token: string;
27
- source: string;
28
- }>;
29
- };
30
- type KeyRiskLike = {
31
- text: string;
32
- category?: string;
33
- } | string;
34
- type RecLike = {
35
- rationale: string;
36
- key_strengths: KeyRiskLike[];
37
- key_risks: KeyRiskLike[];
38
- modality_choice: {
39
- rationale: string;
40
- };
41
- external_citations?: Array<{
42
- id: string;
43
- }>;
44
- };
45
- export declare function validateRecommendationCitations(rec: RecLike, dossier: unknown): RecommendationAudit;
46
- export {};
@@ -1,316 +0,0 @@
1
- // The recommendation agent emits citations as `[dossier: path; path; …]`
2
- // blocks (colon-space, semicolon-separated), sometimes with a trailing
3
- // `external citation: id; id` segment inside the same bracket. The old
4
- // `dossier.path` regex never matched this in production — citations_total
5
- // was always 0. These patterns match the format the agent actually writes.
6
- // The block content may include array index notation like `[0]`, so we
7
- // use a pattern that allows `[digits]` inside the outer brackets.
8
- const DOSSIER_CITE_BLOCK_RE = /\[dossier:\s*((?:[^[\]]|\[\d+\])*)\]/gi;
9
- // External citation IDs are plain strings — no array-index notation like `[n]` — so the simple [^\]]+ class suffices here instead of the array-index-aware pattern used by DOSSIER_CITE_BLOCK_RE.
10
- const EXTERNAL_CITE_BLOCK_RE = /\[external citation:\s*([^\]]+)\]/gi;
11
- // Assumes at most one `external citation:` segment per dossier block (the recommendation prompt produces at most one); a hypothetical second segment in the same block would be parsed imperfectly.
12
- const EXTERNAL_SEGMENT_RE = /external citation:\s*((?:[^[\]]|\[\d+\])*)/i;
13
- const NCT_RE = /\bNCT\d{8}\b/g;
14
- const PMID_RE = /\bPMID[:\s]?(\d{6,9})\b/g;
15
- // Legacy inline format: `dossier.path.foo` written directly in prose, no
16
- // brackets. Still emitted by some recommendation outputs and exercised by
17
- // the regression fixtures, so BOTH this and the `[dossier: …]` block format
18
- // must be supported. This pattern only matches a bare `dossier.x.y` token —
19
- // inside a `[dossier: …]` block the word `dossier` is followed by `:`, not
20
- // `.`/`[`, so the two are disjoint and never double-count.
21
- const DOSSIER_PATH_RE = /\bdossier(?:\.[A-Za-z_][A-Za-z0-9_]*|\[\d+\])+/g;
22
- // Legacy bare external reference: `[ANDA215864]` — an all-caps id in
23
- // brackets. Disjoint from `[dossier: …]` / `[external citation: …]` blocks
24
- // (those start with a lowercase word) and from `[0]` array indices (digits).
25
- const EXTERNAL_ID_RE = /\[([A-Z]{2,}[A-Z0-9_-]+)\]/g;
26
- // Path prefixes the agent uses that are NOT part of the persisted dossier
27
- // body — they refer to the per-section synthesis inputs the agent was
28
- // shown. Citing them is legitimate; resolving them against the dossier is
29
- // not, so they go to `non_dossier_citations` instead of `unresolved`.
30
- const NON_DOSSIER_PREFIXES = ["per_section_synthesis"];
31
- const ORGAN_CATEGORIES = new Set(["high_safety_organ_expression", "broad_expression", "class_liability"]);
32
- /**
33
- * Keyword fallback for detecting organ-level claims when the bullet has
34
- * no structural `category` field (the production
35
- * DossierRecommendationOutputSchema types key_risks as z.array(z.string())
36
- * with no category metadata, so a category check alone would be dead
37
- * code). Matches the regression-test approach in
38
- * [C4] of calcr-target-assessment.test.ts.
39
- */
40
- const ORGAN_KEYWORDS_RE = /\b(kidney|kidneys|renal|nephro\w*|cns|central\s+nervous|brain|hypothalam\w*|cardiac|cardiovascular|hepatic|liver|hepato\w*|pulmonary|respiratory|gastrointestinal)\b/i;
41
- /**
42
- * Bullet-keyword → canonical organ class. The keys mirror the alternatives
43
- * in `ORGAN_KEYWORDS_RE`; values match `target_organ_liabilities[i].organ`,
44
- * which the assembler emits using the canonical safety-panel set
45
- * (`cardiac` | `cns` | `hepatic` | `renal` | `gi` | `respiratory` | ...).
46
- *
47
- * Used to cross-match an organ-level key_risk bullet against the TOL probe
48
- * pass without depending on the bullet text containing the literal JSON
49
- * path `dossier.safety_profile.target_organ_liabilities` — LLM bullets are
50
- * plain English so the path-based check from earlier never matched in
51
- * production.
52
- */
53
- const BULLET_KEYWORD_TO_ORGAN = [
54
- { rx: /\b(kidney|kidneys|renal|nephro\w*)\b/i, organ: "renal" },
55
- { rx: /\b(cns|central\s+nervous|brain|hypothalam\w*)\b/i, organ: "cns" },
56
- { rx: /\b(cardiac|cardiovascular)\b/i, organ: "cardiac" },
57
- { rx: /\b(hepatic|liver|hepato\w*)\b/i, organ: "hepatic" },
58
- { rx: /\b(pulmonary|respiratory)\b/i, organ: "respiratory" },
59
- { rx: /\b(gastrointestinal)\b/i, organ: "gi" },
60
- ];
61
- function extractOrganClassesFromText(text) {
62
- const result = new Set();
63
- for (const { rx, organ } of BULLET_KEYWORD_TO_ORGAN) {
64
- if (rx.test(text))
65
- result.add(organ);
66
- }
67
- return result;
68
- }
69
- function resolvePath(root, path) {
70
- if (!path.startsWith("dossier"))
71
- return undefined;
72
- const stripped = path.replace(/^dossier\.?/, "");
73
- if (stripped === "")
74
- return root;
75
- const parts = stripped.split(/\.|\[(\d+)\]/).filter(Boolean);
76
- return walkWithDataFallback(root, parts);
77
- }
78
- function walkWithDataFallback(root, parts) {
79
- let cur = root;
80
- for (const p of parts) {
81
- if (cur == null)
82
- return undefined;
83
- const next = readPart(cur, p);
84
- if (next !== undefined) {
85
- cur = next;
86
- continue;
87
- }
88
- // The dossier wraps every section as { coverage, data: {...} }. When a
89
- // lookup misses and the current node has a `data` child, descend through
90
- // it transparently — the LLM consistently writes paths without the wrapper
91
- // (e.g. "translational_chain.peak_evidence_tier" instead of
92
- // "translational_chain.data.peak_evidence_tier"). This mirrors how a human
93
- // reader refers to the field; flagging it as unresolved produces noise.
94
- if (typeof cur === "object" && "data" in cur) {
95
- const viaData = readPart(cur.data, p);
96
- if (viaData !== undefined) {
97
- cur = viaData;
98
- continue;
99
- }
100
- }
101
- return undefined;
102
- }
103
- return cur;
104
- }
105
- function readPart(node, p) {
106
- if (node == null)
107
- return undefined;
108
- if (/^\d+$/.test(p))
109
- return Array.isArray(node) ? node[parseInt(p, 10)] : undefined;
110
- return typeof node === "object" ? node[p] : undefined;
111
- }
112
- /**
113
- * Extract dossier-path citations from `text`, supporting two formats:
114
- * - New `[dossier: path; path]` blocks — `;`-separated bare paths, a
115
- * trailing `/ID-list` suffix dropped, `external citation:` segment
116
- * excluded (picked up by `findExternalIds`).
117
- * - Legacy inline `dossier.path.foo` tokens written directly in prose.
118
- *
119
- * Returns `{ path, resolvable }` pairs: `path` is what surfaces in an
120
- * unresolved-citation entry (bare for block tokens, `dossier.`-prefixed for
121
- * legacy tokens — matching what each format's callers expect); `resolvable`
122
- * is always `dossier.`-prefixed so `resolvePath` can walk it.
123
- */
124
- function findPathTokens(text) {
125
- const tokens = [];
126
- // New `[dossier: …]` block format.
127
- for (const block of text.matchAll(DOSSIER_CITE_BLOCK_RE)) {
128
- const dossierPart = block[1].split(/external citation:/i)[0];
129
- for (const raw of dossierPart.split(";")) {
130
- const token = raw.trim().split("/")[0].trim();
131
- if (token.length > 0)
132
- tokens.push({ path: token, resolvable: `dossier.${token}` });
133
- }
134
- }
135
- // Legacy inline `dossier.path.foo` format. The match already carries the
136
- // `dossier.` prefix — keep it in `path` (callers expect the prefixed form
137
- // for this format) and use it directly as `resolvable`.
138
- for (const m of text.matchAll(DOSSIER_PATH_RE)) {
139
- tokens.push({ path: m[0], resolvable: m[0] });
140
- }
141
- return tokens;
142
- }
143
- /**
144
- * Extract external-citation ids. The agent writes them either as a
145
- * standalone `[external citation: id; id]` block or as a trailing segment
146
- * inside a `[dossier: …; external citation: id]` block.
147
- */
148
- function findExternalIds(text) {
149
- const ids = [];
150
- const push = (segment) => {
151
- for (const raw of segment.split(";")) {
152
- const id = raw.trim();
153
- if (id.length > 0)
154
- ids.push(id);
155
- }
156
- };
157
- for (const block of text.matchAll(EXTERNAL_CITE_BLOCK_RE))
158
- push(block[1]);
159
- for (const block of text.matchAll(DOSSIER_CITE_BLOCK_RE)) {
160
- const m = block[1].match(EXTERNAL_SEGMENT_RE);
161
- if (m)
162
- push(m[1]);
163
- }
164
- // Legacy bare `[UPPERCASE_ID]` external reference (e.g. `[ANDA215864]`).
165
- // NCT and PMID references in bracket form are already handled by NCT_RE /
166
- // PMID_RE in the main loop — skip them here to avoid double-processing.
167
- for (const m of text.matchAll(EXTERNAL_ID_RE)) {
168
- const id = m[1];
169
- if (/^NCT\d/.test(id) || /^PMID\d/.test(id))
170
- continue;
171
- ids.push(id);
172
- }
173
- return ids;
174
- }
175
- function normaliseBullet(b) {
176
- return typeof b === "string" ? { text: b } : { text: b.text, category: b.category };
177
- }
178
- function collectClassDrugNames(dossier) {
179
- const rows = dossier?.drug_interactions?.data?.rows ?? [];
180
- return new Set(rows.map((r) => String(r.drug_name ?? "").toUpperCase()).filter(Boolean));
181
- }
182
- function collectKeyPaperPmids(dossier) {
183
- const rows = dossier?.reference_biology?.key_papers?.data?.rows ?? [];
184
- return new Set(rows.map((r) => String(r.pmid)).filter(Boolean));
185
- }
186
- /**
187
- * Extract intervention names from a trial row. Handles two shapes:
188
- * - Flat `interventions: string[]` — the production dossier shape assembled by
189
- * `partitionTrialsByAttribution`.
190
- * - Nested `armsInterventionsModule.interventions: Array<{ name: string }>` — the
191
- * raw CT.gov v2 JSON shape that may appear in test fixtures or future collector
192
- * output before the assembler flattens it.
193
- */
194
- function extractInterventionNames(row) {
195
- // Production flat shape (priority)
196
- if (Array.isArray(row.interventions)) {
197
- return row.interventions.map((iv) => (typeof iv === "string" ? iv.toUpperCase() : String(iv?.name ?? "").toUpperCase()));
198
- }
199
- // Raw CT.gov nested shape fallback
200
- const nested = row?.armsInterventionsModule?.interventions;
201
- if (Array.isArray(nested)) {
202
- return nested.map((iv) => String(iv?.name ?? "").toUpperCase());
203
- }
204
- return [];
205
- }
206
- function findTrialInterventions(dossier, nctId) {
207
- const trialSets = [
208
- dossier?.clinical_development?.trials?.data?.rows ?? [],
209
- dossier?.clinical_development?.failed_trials?.data?.rows ?? [],
210
- dossier?.analytics?.discovery_trials?.data?.rows ?? [],
211
- ];
212
- for (const set of trialSets) {
213
- const row = set.find((t) => t.nct_id === nctId);
214
- if (!row)
215
- continue;
216
- return extractInterventionNames(row);
217
- }
218
- return [];
219
- }
220
- export function validateRecommendationCitations(rec, dossier) {
221
- const dossierView = dossier;
222
- const unresolved = [];
223
- const nonDossier = [];
224
- let total = 0;
225
- const externalIds = new Set((rec.external_citations ?? []).map((c) => c.id));
226
- const surfaces = [
227
- { surface: "rationale", text: rec.rationale },
228
- { surface: "modality_choice", text: rec.modality_choice.rationale },
229
- ...rec.key_strengths.map((b) => ({ surface: "key_strengths", text: normaliseBullet(b).text })),
230
- ...rec.key_risks.map((b) => ({ surface: "key_risks", text: normaliseBullet(b).text })),
231
- ];
232
- const classDrugNames = collectClassDrugNames(dossierView);
233
- const keyPmids = collectKeyPaperPmids(dossierView);
234
- for (const { surface, text } of surfaces) {
235
- for (const { path, resolvable } of findPathTokens(text)) {
236
- total += 1;
237
- // Legacy-format tokens carry a `dossier.` prefix; block-format tokens
238
- // are bare. Strip the prefix so the non-dossier-prefix check works for
239
- // both formats.
240
- const barePath = path.startsWith("dossier.") ? path.slice("dossier.".length) : path;
241
- if (NON_DOSSIER_PREFIXES.some((p) => barePath === p || barePath.startsWith(`${p}.`) || barePath.startsWith(`${p}[`))) {
242
- nonDossier.push({ token: path, source: surface });
243
- continue;
244
- }
245
- const resolved = resolvePath(dossier, resolvable);
246
- if (resolved === undefined || resolved === null) {
247
- unresolved.push({ surface, path, excerpt: text.slice(0, 200) });
248
- }
249
- }
250
- for (const id of findExternalIds(text)) {
251
- if (!externalIds.has(id)) {
252
- unresolved.push({ surface: "external_missing", id, excerpt: text.slice(0, 200) });
253
- }
254
- }
255
- for (const m of text.matchAll(NCT_RE)) {
256
- const nctId = m[0];
257
- const interventions = findTrialInterventions(dossierView, nctId);
258
- // Only flag wrong-class when the trial IS present in the dossier
259
- // and its interventions don't reference the class. Trials absent
260
- // from the collected sets are unverifiable — flagging them would
261
- // produce false positives for any on-class trial that the
262
- // synthesis agent cited but the discovery collector missed.
263
- if (interventions.length === 0)
264
- continue;
265
- // Skip when no class-drug catalog is available — without a reference
266
- // set (early-stage / first-in-class targets, or drug_interactions
267
- // collector returning queried_no_data) every cited trial would
268
- // otherwise flag as wrong-class regardless of its actual class.
269
- if (classDrugNames.size === 0)
270
- continue;
271
- const referencesClassDrug = interventions.some((iv) => [...classDrugNames].some((name) => iv.includes(name)));
272
- if (!referencesClassDrug) {
273
- unresolved.push({ surface: "nct_wrong_class", id: nctId, excerpt: text.slice(0, 200) });
274
- }
275
- }
276
- for (const m of text.matchAll(PMID_RE)) {
277
- const pmid = m[1];
278
- if (!keyPmids.has(pmid)) {
279
- unresolved.push({ surface: "pmid_not_in_key_papers", id: pmid, excerpt: text.slice(0, 200) });
280
- }
281
- }
282
- }
283
- const organLiabilities = dossierView?.safety_profile?.target_organ_liabilities?.data?.rows ?? [];
284
- const tolOrgans = new Set(organLiabilities
285
- .map((t) => String(t?.organ ?? "")
286
- .toLowerCase()
287
- .trim())
288
- .filter((o) => o.length > 0));
289
- for (const b of rec.key_risks ?? []) {
290
- const norm = normaliseBullet(b);
291
- // Detect organ-level claims by structural category when available
292
- // (typed bullets) OR by keyword match on the text (plain-string
293
- // bullets — the production shape).
294
- const isOrganClaim = (norm.category != null && ORGAN_CATEGORIES.has(norm.category)) || ORGAN_KEYWORDS_RE.test(norm.text);
295
- if (!isOrganClaim)
296
- continue;
297
- // "Probe pass" = the TOL section has an entry whose organ matches the
298
- // organ class mentioned in the bullet. Production bullets are plain
299
- // English so we extract the organ class via keyword matching and
300
- // cross-reference against the canonical TOL organ field.
301
- const bulletOrgans = extractOrganClassesFromText(norm.text);
302
- const citesTOL = bulletOrgans.size > 0 && [...bulletOrgans].some((o) => tolOrgans.has(o));
303
- if (!citesTOL) {
304
- unresolved.push({
305
- surface: "organ_claim_without_probe_pass",
306
- excerpt: norm.text.slice(0, 200),
307
- bullet_category: norm.category ?? "inferred_from_text",
308
- });
309
- }
310
- }
311
- return {
312
- citations_total: total,
313
- citations_unresolved: unresolved,
314
- non_dossier_citations: nonDossier,
315
- };
316
- }
@@ -1,65 +0,0 @@
1
- /**
2
- * Per-row LLM clinical-consequence annotator with Postgres caching.
3
- *
4
- * Any off-target row missing a `clinical_consequence` gets annotated by a
5
- * focused tool-less LLM specialist that returns ONE sentence + provenance.
6
- * Results are cached in `cortex_off_target_annotations` keyed by
7
- * (primary_target_gene, off_target_key) so repeat assessments reuse prior
8
- * work.
9
- *
10
- * Cache miss path: when a `deps` bundle is supplied, build a compact JSON
11
- * prompt, run a single-shot harness agent loop (no tools, max 2 iterations),
12
- * parse JSON out of the final assistant text, INSERT on success, return the
13
- * consequence. With no `deps`, the function returns null on cache miss
14
- * (no LLM call). Cache hit path: a single primary-key lookup, no LLM call.
15
- * Per-row failures (LLM, schema validation, DB) are swallowed — annotation
16
- * is supplementary; the dossier renders fine with null.
17
- */
18
- import { z } from "zod";
19
- import type { Pool } from "pg";
20
- import type { AgentSession } from "../../../auth/types.js";
21
- import type { AgentChat } from "../../../providers/types.js";
22
- import type { UsageRecorder } from "../../../billing/usage-recorder.js";
23
- export declare const OffTargetAnnotationSchema: z.ZodObject<{
24
- clinical_consequence: z.ZodString;
25
- provenance: z.ZodString;
26
- }, z.core.$strip>;
27
- export type OffTargetAnnotation = z.infer<typeof OffTargetAnnotationSchema>;
28
- export interface ClinicalConsequenceAnnotatorDeps {
29
- readonly provider: AgentChat;
30
- readonly session: AgentSession;
31
- readonly model: string;
32
- /** LLM usage-accounting seam for the annotator loop; omitted falls back to the no-op recorder. */
33
- readonly usageRecorder?: UsageRecorder;
34
- }
35
- export interface AnnotationInput {
36
- primaryTargetGene: string;
37
- offTargetId: string | null;
38
- offTargetName: string;
39
- offTargetAccession: string | null;
40
- pchembl: number;
41
- /** Optional one-line family-relationship hint (e.g., "obligate cofactor"). */
42
- context?: string;
43
- }
44
- /**
45
- * Annotate a single off-target row. Returns null on any failure (cache
46
- * miss + no deps, cache miss + LLM unavailable, schema validation failure,
47
- * DB unreachable, etc.) — callers must treat null as "no annotation" and
48
- * leave `clinical_consequence` as their original fallback.
49
- */
50
- export declare function annotateClinicalConsequence(pool: Pool, input: AnnotationInput, deps?: ClinicalConsequenceAnnotatorDeps): Promise<OffTargetAnnotation | null>;
51
- /**
52
- * Annotate every row in an off-target panel whose `clinical_consequence`
53
- * is currently null. Mutates the supplied arrays in-place. Pure assembly-
54
- * time helper — no return value.
55
- *
56
- * Per-row LLM calls are bounded by the "annotation-llm" host budget (4 in
57
- * flight). Cache hits skip the LLM entirely and consume only a cheap
58
- * Postgres lookup, so the budget gate is held briefly for those rows.
59
- * Without `deps`, the panel is annotated cache-only — rows missing a
60
- * cached entry remain with their original fallback.
61
- */
62
- export declare function annotateOffTargetPanel(pool: Pool, panel: {
63
- rows: Array<Record<string, unknown>>;
64
- excluded_rows: Array<Record<string, unknown>>;
65
- }, primaryTargetGene: string, deps?: ClinicalConsequenceAnnotatorDeps): Promise<void>;
@@ -1,174 +0,0 @@
1
- /**
2
- * Per-row LLM clinical-consequence annotator with Postgres caching.
3
- *
4
- * Any off-target row missing a `clinical_consequence` gets annotated by a
5
- * focused tool-less LLM specialist that returns ONE sentence + provenance.
6
- * Results are cached in `cortex_off_target_annotations` keyed by
7
- * (primary_target_gene, off_target_key) so repeat assessments reuse prior
8
- * work.
9
- *
10
- * Cache miss path: when a `deps` bundle is supplied, build a compact JSON
11
- * prompt, run a single-shot harness agent loop (no tools, max 2 iterations),
12
- * parse JSON out of the final assistant text, INSERT on success, return the
13
- * consequence. With no `deps`, the function returns null on cache miss
14
- * (no LLM call). Cache hit path: a single primary-key lookup, no LLM call.
15
- * Per-row failures (LLM, schema validation, DB) are swallowed — annotation
16
- * is supplementary; the dossier renders fine with null.
17
- */
18
- import { z } from "zod";
19
- import { withHost } from "../../../lib/host-concurrency.js";
20
- import { tryQuery, tryMutation } from "../../../lib/db-result.js";
21
- import { offTargetAnnotatorPrompt } from "../../../prompts/target-assessment/off-target-annotator.js";
22
- import { composeSystemPrompt } from "../../../agents/system-prompt.js";
23
- import { forSubAgent } from "../../../auth/types.js";
24
- import { finalText, runAgent } from "../../../loop/run-agent.js";
25
- import { passthroughStep } from "../../../loop/run-step.js";
26
- export const OffTargetAnnotationSchema = z.object({
27
- clinical_consequence: z.string().min(10),
28
- provenance: z.string().min(3),
29
- });
30
- function cacheKey(input) {
31
- const id = input.offTargetId?.trim();
32
- if (id)
33
- return id;
34
- const name = input.offTargetName.trim().toLowerCase();
35
- return name ? `name:${name}` : null;
36
- }
37
- async function readCache(pool, primaryTargetGene, key) {
38
- // Annotation is supplementary — a cache-read failure must NOT surface. The
39
- // `DbError` is collapsed to `null` (treated as a cache miss) via `.unwrapOr`.
40
- return tryQuery("offTargetAnnotations.readCache", async () => {
41
- const res = await pool.query(`SELECT clinical_consequence, provenance
42
- FROM cortex_off_target_annotations
43
- WHERE primary_target_gene = $1 AND off_target_key = $2
44
- LIMIT 1`, [primaryTargetGene, key]);
45
- if (res.rows.length === 0)
46
- return null;
47
- const row = res.rows[0];
48
- return {
49
- clinical_consequence: row.clinical_consequence,
50
- provenance: row.provenance ?? "",
51
- };
52
- }).unwrapOr(null);
53
- }
54
- async function writeCache(pool, primaryTargetGene, key, offTargetName, annotation, model) {
55
- // Cache-write failures must not affect the assembly result — the `DbError`
56
- // is swallowed (both branches no-op) so annotation never surfaces a DB fault.
57
- await tryMutation("offTargetAnnotations.writeCache", async () => {
58
- await pool.query(`INSERT INTO cortex_off_target_annotations
59
- (primary_target_gene, off_target_key, off_target_name, clinical_consequence, provenance, model)
60
- VALUES ($1, $2, $3, $4, $5, $6)
61
- ON CONFLICT (primary_target_gene, off_target_key) DO NOTHING`, [primaryTargetGene, key, offTargetName, annotation.clinical_consequence, annotation.provenance, model]);
62
- }).match(() => { }, () => { });
63
- }
64
- function buildPrompt(input) {
65
- return JSON.stringify({
66
- primary_target_gene: input.primaryTargetGene,
67
- off_target_id: input.offTargetId,
68
- off_target_name: input.offTargetName,
69
- off_target_accession: input.offTargetAccession,
70
- pchembl: input.pchembl,
71
- context: input.context ?? null,
72
- }, null, 2);
73
- }
74
- /**
75
- * Best-effort JSON extraction from an LLM text response. Strips fenced
76
- * code blocks and surrounding prose so a slightly-out-of-spec reply still
77
- * parses.
78
- */
79
- function extractJsonObject(text) {
80
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
81
- const candidate = fenced ? fenced[1] : text;
82
- const start = candidate.indexOf("{");
83
- const end = candidate.lastIndexOf("}");
84
- if (start === -1 || end === -1 || end < start)
85
- return null;
86
- try {
87
- return JSON.parse(candidate.slice(start, end + 1));
88
- }
89
- catch {
90
- return null;
91
- }
92
- }
93
- const ANNOTATOR_AGENT_ID = "off-target-annotator";
94
- function buildAnnotatorAgent(model) {
95
- return {
96
- id: ANNOTATOR_AGENT_ID,
97
- systemPrompt: composeSystemPrompt(offTargetAnnotatorPrompt),
98
- model,
99
- tools: [],
100
- maxIterations: 2,
101
- };
102
- }
103
- /**
104
- * Annotate a single off-target row. Returns null on any failure (cache
105
- * miss + no deps, cache miss + LLM unavailable, schema validation failure,
106
- * DB unreachable, etc.) — callers must treat null as "no annotation" and
107
- * leave `clinical_consequence` as their original fallback.
108
- */
109
- export async function annotateClinicalConsequence(pool, input, deps) {
110
- if (!input.primaryTargetGene)
111
- return null;
112
- const key = cacheKey(input);
113
- if (!key)
114
- return null;
115
- const cached = await readCache(pool, input.primaryTargetGene, key);
116
- if (cached)
117
- return cached;
118
- if (!deps)
119
- return null;
120
- try {
121
- const agent = buildAnnotatorAgent(deps.model);
122
- const session = forSubAgent(deps.session, ANNOTATOR_AGENT_ID);
123
- const controller = new AbortController();
124
- const { messages } = await runAgent(agent, [{ role: "user", content: buildPrompt(input) }], session, {
125
- provider: deps.provider,
126
- signal: controller.signal,
127
- emit: () => { },
128
- runStep: passthroughStep,
129
- usageRecorder: deps.usageRecorder,
130
- });
131
- const raw = extractJsonObject(finalText(messages));
132
- if (raw === null)
133
- return null;
134
- const parsed = OffTargetAnnotationSchema.safeParse(raw);
135
- if (!parsed.success)
136
- return null;
137
- await writeCache(pool, input.primaryTargetGene, key, input.offTargetName, parsed.data, deps.model);
138
- return parsed.data;
139
- }
140
- catch {
141
- return null;
142
- }
143
- }
144
- /**
145
- * Annotate every row in an off-target panel whose `clinical_consequence`
146
- * is currently null. Mutates the supplied arrays in-place. Pure assembly-
147
- * time helper — no return value.
148
- *
149
- * Per-row LLM calls are bounded by the "annotation-llm" host budget (4 in
150
- * flight). Cache hits skip the LLM entirely and consume only a cheap
151
- * Postgres lookup, so the budget gate is held briefly for those rows.
152
- * Without `deps`, the panel is annotated cache-only — rows missing a
153
- * cached entry remain with their original fallback.
154
- */
155
- export async function annotateOffTargetPanel(pool, panel, primaryTargetGene, deps) {
156
- if (!primaryTargetGene)
157
- return;
158
- const allRows = [...panel.rows, ...panel.excluded_rows];
159
- const needsAnnotation = allRows.filter((r) => r && (r.clinical_consequence == null || r.clinical_consequence === ""));
160
- if (needsAnnotation.length === 0)
161
- return;
162
- await Promise.all(needsAnnotation.map((r) => withHost("annotation-llm", async () => {
163
- const annotation = await annotateClinicalConsequence(pool, {
164
- primaryTargetGene,
165
- offTargetId: typeof r.off_target_id === "string" ? r.off_target_id : null,
166
- offTargetName: typeof r.off_target_name === "string" ? r.off_target_name : "",
167
- offTargetAccession: typeof r.accession === "string" ? r.accession : null,
168
- pchembl: typeof r.pchembl === "number" ? r.pchembl : 0,
169
- context: r.relationship === "obligate_cofactor" ? "obligate cofactor with the primary protein" : undefined,
170
- }, deps);
171
- if (annotation)
172
- r.clinical_consequence = annotation.clinical_consequence;
173
- })));
174
- }
@@ -1,44 +0,0 @@
1
- import type { ClinicalTrialAttribution } from "@inflexa-ai/harness/contracts/target-dossier.js";
2
- type TrialLike = {
3
- nctId: string;
4
- title: string;
5
- officialTitle?: string | null;
6
- conditions?: string[];
7
- interventions?: string[];
8
- interventionDetails?: Array<{
9
- name: string;
10
- type?: string | null;
11
- description?: string | null;
12
- otherNames?: string[];
13
- }>;
14
- studyType?: string | null;
15
- primaryPurpose?: string | null;
16
- briefSummary?: string | null;
17
- detailedDescription?: string | null;
18
- };
19
- export type KnownClassDrug = {
20
- name: string;
21
- moleculeChemblId?: string | null;
22
- targetUniprots?: string[];
23
- };
24
- export type TherapeuticProgram = {
25
- programId: string;
26
- name: string;
27
- targetSymbol: string;
28
- targetUniprot?: string | null;
29
- modality?: string | null;
30
- nctIds?: string[];
31
- };
32
- export type ClinicalEvidenceAttributionContext = {
33
- assessmentSymbol: string;
34
- assessmentUniprot: string;
35
- familyUniprots: string[];
36
- knownClassDrugs: KnownClassDrug[];
37
- therapeuticPrograms: TherapeuticProgram[];
38
- };
39
- export type ClinicalEvidenceAttributionResult = {
40
- attribution: ClinicalTrialAttribution;
41
- eligible_for_toxicology_aggregation: boolean;
42
- };
43
- export declare function classifyClinicalEvidenceTrial(trial: TrialLike, ctx: ClinicalEvidenceAttributionContext): ClinicalEvidenceAttributionResult;
44
- export {};