@inflexa-ai/harness 0.1.1 → 0.3.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 (330) hide show
  1. package/README.md +48 -0
  2. package/dist/agents/conversation-agent.d.ts +15 -11
  3. package/dist/agents/conversation-agent.js +31 -34
  4. package/dist/agents/report-builder.js +2 -1
  5. package/dist/agents/sandbox/bulk-transcriptomics-agent.js +1 -1
  6. package/dist/agents/sandbox/cheminformatics-agent.js +1 -19
  7. package/dist/agents/sandbox/chromatin-agent.js +1 -10
  8. package/dist/agents/sandbox/dna-methylation-agent.js +1 -10
  9. package/dist/agents/sandbox/drug-repurposing-agent.js +4 -14
  10. package/dist/agents/sandbox/enrichment-agent.js +1 -10
  11. package/dist/agents/sandbox/ephemeral-executor.js +1 -10
  12. package/dist/agents/sandbox/genomic-variant-agent.js +1 -3
  13. package/dist/agents/sandbox/immune-profiling-agent.js +1 -13
  14. package/dist/agents/sandbox/metabolomics-agent.js +1 -1
  15. package/dist/agents/sandbox/microbiome-agent.js +1 -1
  16. package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -12
  17. package/dist/agents/sandbox/multimodal-sc-agent.js +1 -1
  18. package/dist/agents/sandbox/network-agent.js +1 -10
  19. package/dist/agents/sandbox/pkpd-clinical-response-agent.js +2 -8
  20. package/dist/agents/sandbox/proteomics-agent.js +1 -1
  21. package/dist/agents/sandbox/scientific-executor.js +1 -10
  22. package/dist/agents/sandbox/shared.d.ts +20 -6
  23. package/dist/agents/sandbox/shared.js +50 -39
  24. package/dist/agents/sandbox/single-cell-agent.js +1 -1
  25. package/dist/agents/sandbox/spatial-omics-agent.js +1 -1
  26. package/dist/agents/sandbox/statistical-modeling-agent.js +1 -1
  27. package/dist/agents/sandbox/translational-safety-agent.js +3 -9
  28. package/dist/agents/sandbox/types.d.ts +1 -1
  29. package/dist/agents/system-prompt.d.ts +37 -11
  30. package/dist/agents/system-prompt.js +35 -12
  31. package/dist/app/chat-turn.d.ts +3 -0
  32. package/dist/app/chat-turn.js +3 -1
  33. package/dist/app/data-profile-orientation.d.ts +34 -0
  34. package/dist/app/data-profile-orientation.js +117 -0
  35. package/dist/app/synthesize-run.d.ts +3 -0
  36. package/dist/app/synthesize-run.js +3 -1
  37. package/dist/contracts/schemas/chat-parts.d.ts +9 -9
  38. package/dist/execution/artifact-metadata.d.ts +3 -0
  39. package/dist/execution/artifact-metadata.js +10 -3
  40. package/dist/execution/artifact-registration.d.ts +2 -1
  41. package/dist/execution/artifact-registration.js +8 -2
  42. package/dist/execution/ephemeral-runner.d.ts +16 -0
  43. package/dist/execution/ephemeral-runner.js +31 -5
  44. package/dist/execution/post-step-pipeline.d.ts +3 -0
  45. package/dist/execution/post-step-pipeline.js +24 -4
  46. package/dist/execution/reconcile-manifest.d.ts +7 -0
  47. package/dist/execution/reconcile-manifest.js +19 -5
  48. package/dist/execution/report-runner.d.ts +3 -0
  49. package/dist/execution/report-runner.js +4 -4
  50. package/dist/execution/run-synthesis.js +20 -19
  51. package/dist/execution/step-summary.d.ts +3 -0
  52. package/dist/execution/step-summary.js +6 -3
  53. package/dist/index.d.ts +9 -2
  54. package/dist/index.js +24 -5
  55. package/dist/lib/chrome.d.ts +4 -1
  56. package/dist/lib/chrome.js +10 -7
  57. package/dist/lib/console-logger.d.ts +19 -0
  58. package/dist/lib/console-logger.js +62 -0
  59. package/dist/lib/logger.d.ts +88 -0
  60. package/dist/lib/logger.js +37 -0
  61. package/dist/lib/otel.d.ts +2 -1
  62. package/dist/lib/otel.js +8 -3
  63. package/dist/loop/__fixtures__/scripted-provider.d.ts +2 -2
  64. package/dist/loop/__fixtures__/scripted-provider.js +2 -1
  65. package/dist/loop/metrics.d.ts +33 -1
  66. package/dist/loop/metrics.js +56 -2
  67. package/dist/loop/run-agent.d.ts +13 -1
  68. package/dist/loop/run-agent.js +19 -4
  69. package/dist/memory/card-builders.d.ts +8 -1
  70. package/dist/memory/card-builders.js +20 -3
  71. package/dist/memory/reconstruct-cards.js +4 -3
  72. package/dist/memory/thread-history.d.ts +27 -2
  73. package/dist/memory/thread-history.js +45 -5
  74. package/dist/memory/working-memory.d.ts +93 -10
  75. package/dist/memory/working-memory.js +220 -84
  76. package/dist/prompts/SOUL.d.ts +22 -9
  77. package/dist/prompts/SOUL.js +56 -37
  78. package/dist/prompts/analogical-reasoner.d.ts +1 -1
  79. package/dist/prompts/analogical-reasoner.js +2 -2
  80. package/dist/prompts/briefing.d.ts +100 -0
  81. package/dist/prompts/briefing.js +166 -0
  82. package/dist/prompts/conversation.d.ts +1 -1
  83. package/dist/prompts/conversation.js +94 -440
  84. package/dist/prompts/literature-reviewer.d.ts +1 -1
  85. package/dist/prompts/literature-reviewer.js +10 -10
  86. package/dist/prompts/planner.js +28 -59
  87. package/dist/prompts/report-builder.d.ts +1 -1
  88. package/dist/prompts/report-builder.js +71 -24
  89. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  90. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +2 -7
  91. package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
  92. package/dist/prompts/sandbox/cheminformatics-agent.js +11 -15
  93. package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
  94. package/dist/prompts/sandbox/chromatin-agent.js +3 -7
  95. package/dist/prompts/sandbox/data-profiler.d.ts +1 -1
  96. package/dist/prompts/sandbox/data-profiler.js +13 -8
  97. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  98. package/dist/prompts/sandbox/dna-methylation-agent.js +2 -7
  99. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  100. package/dist/prompts/sandbox/drug-repurposing-agent.js +15 -16
  101. package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
  102. package/dist/prompts/sandbox/enrichment-agent.js +2 -9
  103. package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
  104. package/dist/prompts/sandbox/genomic-variant-agent.js +2 -6
  105. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  106. package/dist/prompts/sandbox/immune-profiling-agent.js +3 -8
  107. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  108. package/dist/prompts/sandbox/metabolomics-agent.js +1 -7
  109. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  110. package/dist/prompts/sandbox/microbiome-agent.js +2 -7
  111. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  112. package/dist/prompts/sandbox/multi-omics-integration-agent.js +2 -8
  113. package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
  114. package/dist/prompts/sandbox/multimodal-sc-agent.js +2 -7
  115. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  116. package/dist/prompts/sandbox/network-agent.js +2 -7
  117. package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
  118. package/dist/prompts/sandbox/proteomics-agent.js +2 -9
  119. package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
  120. package/dist/prompts/sandbox/single-cell-agent.js +2 -9
  121. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  122. package/dist/prompts/sandbox/spatial-omics-agent.js +3 -6
  123. package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
  124. package/dist/prompts/sandbox/statistical-modeling-agent.js +2 -7
  125. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  126. package/dist/prompts/sandbox/translational-safety-agent.js +13 -14
  127. package/dist/prompts/sandbox-standards.d.ts +22 -14
  128. package/dist/prompts/sandbox-standards.js +91 -71
  129. package/dist/prompts/synthesis-agent.d.ts +1 -1
  130. package/dist/prompts/synthesis-agent.js +33 -131
  131. package/dist/provenance/collector.js +1 -1
  132. package/dist/providers/ai-sdk.js +31 -4
  133. package/dist/providers/prompt-cache.d.ts +74 -0
  134. package/dist/providers/prompt-cache.js +77 -0
  135. package/dist/providers/types.d.ts +33 -0
  136. package/dist/reference-data/catalog.d.ts +131 -0
  137. package/dist/reference-data/catalog.js +348 -0
  138. package/dist/reference-data/receipt.d.ts +41 -0
  139. package/dist/reference-data/receipt.js +40 -0
  140. package/dist/runtime/boot.d.ts +2 -2
  141. package/dist/runtime/boot.js +1 -1
  142. package/dist/runtime/connection-budget.d.ts +2 -2
  143. package/dist/runtime/connection-budget.js +5 -3
  144. package/dist/runtime/dbos.d.ts +7 -7
  145. package/dist/runtime/dbos.js +14 -11
  146. package/dist/runtime/shutdown.d.ts +2 -2
  147. package/dist/runtime/shutdown.js +4 -3
  148. package/dist/sandbox/create-sandbox.d.ts +2 -2
  149. package/dist/sandbox/docker-client.d.ts +2 -2
  150. package/dist/sandbox/docker-client.js +4 -1
  151. package/dist/sandbox/k8s-client.d.ts +3 -0
  152. package/dist/sandbox/k8s-client.js +5 -4
  153. package/dist/sandbox/notification-sweep.d.ts +3 -3
  154. package/dist/sandbox/notification-sweep.js +4 -2
  155. package/dist/sandbox/reaper.d.ts +3 -3
  156. package/dist/sandbox/reaper.js +7 -4
  157. package/dist/sandbox/watchdog.d.ts +3 -3
  158. package/dist/sandbox/watchdog.js +8 -2
  159. package/dist/schemas/data-profile-schemas.js +5 -3
  160. package/dist/schemas/plan-schemas.d.ts +21 -21
  161. package/dist/schemas/plan-schemas.js +9 -3
  162. package/dist/schemas/run-synthesis.d.ts +8 -4
  163. package/dist/schemas/run-synthesis.js +92 -56
  164. package/dist/schemas/validate-plan.js +1 -1
  165. package/dist/schemas/workflow-state.d.ts +7 -1
  166. package/dist/schemas/workflow-state.js +33 -19
  167. package/dist/state/artifacts.d.ts +13 -0
  168. package/dist/state/artifacts.js +27 -0
  169. package/dist/state/data-profile.d.ts +64 -5
  170. package/dist/state/index.d.ts +3 -3
  171. package/dist/state/index.js +1 -1
  172. package/dist/state/init.d.ts +2 -1
  173. package/dist/state/init.js +5 -6
  174. package/dist/tasks/data-profile.d.ts +25 -2
  175. package/dist/tasks/data-profile.js +95 -39
  176. package/dist/tools/bio/chembl.d.ts +38 -0
  177. package/dist/tools/bio/chembl.js +138 -0
  178. package/dist/tools/bio/comptox.d.ts +50 -0
  179. package/dist/tools/bio/comptox.js +149 -0
  180. package/dist/tools/bio/get-impc-ko-profile.js +9 -6
  181. package/dist/tools/bio/index.d.ts +5 -17
  182. package/dist/tools/bio/index.js +5 -17
  183. package/dist/tools/bio/keys.d.ts +14 -9
  184. package/dist/tools/bio/keys.js +16 -16
  185. package/dist/tools/bio/opentargets.d.ts +38 -0
  186. package/dist/tools/bio/opentargets.js +89 -0
  187. package/dist/tools/bio/pubchem.d.ts +29 -0
  188. package/dist/tools/bio/pubchem.js +92 -0
  189. package/dist/tools/bio/pubmed.d.ts +51 -0
  190. package/dist/tools/bio/pubmed.js +100 -0
  191. package/dist/tools/bio/search-bgee-expression.js +12 -6
  192. package/dist/tools/bio/search-clinical-trials.js +16 -5
  193. package/dist/tools/bio/search-dgidb.d.ts +11 -1
  194. package/dist/tools/bio/search-dgidb.js +92 -78
  195. package/dist/tools/bio/search-faers.js +8 -4
  196. package/dist/tools/bio/search-geo-datasets.js +20 -6
  197. package/dist/tools/bio/search-gwas-catalog.d.ts +1 -1
  198. package/dist/tools/bio/search-pharmgkb.js +10 -4
  199. package/dist/tools/display/index.d.ts +1 -0
  200. package/dist/tools/display/index.js +1 -0
  201. package/dist/tools/display/normalize-echart-spec.d.ts +61 -0
  202. package/dist/tools/display/normalize-echart-spec.js +211 -0
  203. package/dist/tools/display/show-user.d.ts +6 -1
  204. package/dist/tools/display/show-user.js +25 -12
  205. package/dist/tools/execute-plan.js +5 -2
  206. package/dist/tools/iterate-report.d.ts +171 -118
  207. package/dist/tools/iterate-report.js +336 -96
  208. package/dist/tools/lib/chembl-client.d.ts +12 -0
  209. package/dist/tools/lib/chembl-client.js +127 -0
  210. package/dist/tools/lib/ctx-ops.d.ts +224 -0
  211. package/dist/tools/lib/ctx-ops.js +450 -0
  212. package/dist/tools/lib/iuphar-client.js +10 -1
  213. package/dist/tools/lib/openfda-client.js +11 -3
  214. package/dist/tools/lib/pathway-client.js +10 -4
  215. package/dist/tools/lib/pubchem-ops.d.ts +74 -0
  216. package/dist/tools/lib/pubchem-ops.js +224 -0
  217. package/dist/tools/memory/update-working-memory.d.ts +45 -7
  218. package/dist/tools/memory/update-working-memory.js +111 -41
  219. package/dist/tools/report/index.d.ts +3 -2
  220. package/dist/tools/report/index.js +3 -2
  221. package/dist/tools/research/generate-analogy-report.js +25 -12
  222. package/dist/tools/research/generate-plan.d.ts +12 -5
  223. package/dist/tools/research/generate-plan.js +168 -37
  224. package/dist/tools/research/index.d.ts +1 -0
  225. package/dist/tools/research/index.js +1 -0
  226. package/dist/tools/research/inspect-data-profile.d.ts +85 -0
  227. package/dist/tools/research/inspect-data-profile.js +161 -0
  228. package/dist/tools/research/literature-reviewer.d.ts +3 -0
  229. package/dist/tools/research/literature-reviewer.js +5 -6
  230. package/dist/tools/sandbox/list-available-packages.d.ts +79 -5
  231. package/dist/tools/sandbox/list-available-packages.js +152 -10
  232. package/dist/tools/sandbox/list-available-refs.d.ts +141 -5
  233. package/dist/tools/sandbox/list-available-refs.js +647 -134
  234. package/dist/tools/sandbox/report-blocker.d.ts +39 -9
  235. package/dist/tools/sandbox/report-blocker.js +43 -20
  236. package/dist/tools/workspace/execute-command.d.ts +3 -0
  237. package/dist/tools/workspace/execute-command.js +3 -1
  238. package/dist/tools/workspace/read-file.js +4 -5
  239. package/dist/tools/workspace/run-exec.d.ts +4 -4
  240. package/dist/tools/workspace/run-exec.js +4 -4
  241. package/dist/tools/workspace/show-file.js +10 -6
  242. package/dist/tools/workspace/show-plan.js +12 -6
  243. package/dist/tools/workspace/workspace-search.d.ts +1 -1
  244. package/dist/tools/workspace/workspace-search.js +11 -5
  245. package/dist/workflows/execute-analysis.d.ts +46 -4
  246. package/dist/workflows/execute-analysis.js +118 -13
  247. package/dist/workflows/execute-target-assessment.d.ts +3 -0
  248. package/dist/workflows/execute-target-assessment.js +19 -15
  249. package/dist/workflows/sandbox-step.d.ts +9 -0
  250. package/dist/workflows/sandbox-step.js +39 -17
  251. package/dist/workflows/target-assessment/lib/citation-validator.js +2 -2
  252. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.js +2 -1
  253. package/dist/workflows/target-assessment/lib/llm-step.d.ts +3 -0
  254. package/dist/workflows/target-assessment/lib/llm-step.js +3 -1
  255. package/dist/workflows/target-assessment/lib/structured-llm.d.ts +3 -0
  256. package/dist/workflows/target-assessment/lib/structured-llm.js +1 -0
  257. package/dist/workflows/target-assessment/phase5-persist.d.ts +3 -0
  258. package/dist/workflows/target-assessment/phase5-persist.js +8 -3
  259. package/dist/workflows/target-assessment/progress.d.ts +2 -1
  260. package/dist/workflows/target-assessment/progress.js +4 -3
  261. package/dist/workspace/filesystem.js +163 -74
  262. package/dist/workspace/paths.d.ts +2 -2
  263. package/dist/workspace/paths.js +2 -2
  264. package/package.json +95 -96
  265. package/dist/execution/filesystem-artifact-registry.d.ts +0 -57
  266. package/dist/execution/filesystem-artifact-registry.js +0 -92
  267. package/dist/lib/identifiers.d.ts +0 -8
  268. package/dist/lib/identifiers.js +0 -10
  269. package/dist/lib/mime-detect.d.ts +0 -17
  270. package/dist/lib/mime-detect.js +0 -29
  271. package/dist/lib/streamed-download.d.ts +0 -28
  272. package/dist/lib/streamed-download.js +0 -111
  273. package/dist/lib/trace-context.d.ts +0 -11
  274. package/dist/lib/trace-context.js +0 -49
  275. package/dist/providers/llm-capabilities.d.ts +0 -33
  276. package/dist/providers/llm-capabilities.js +0 -58
  277. package/dist/schemas/render-step-prompt.d.ts +0 -32
  278. package/dist/schemas/render-step-prompt.js +0 -76
  279. package/dist/tools/bio/find-approval-precedent.d.ts +0 -32
  280. package/dist/tools/bio/find-approval-precedent.js +0 -72
  281. package/dist/tools/bio/get-article-details.d.ts +0 -11
  282. package/dist/tools/bio/get-article-details.js +0 -20
  283. package/dist/tools/bio/get-article-full-text.d.ts +0 -19
  284. package/dist/tools/bio/get-article-full-text.js +0 -32
  285. package/dist/tools/bio/get-bioactivity.d.ts +0 -11
  286. package/dist/tools/bio/get-bioactivity.js +0 -23
  287. package/dist/tools/bio/get-drug-info.d.ts +0 -9
  288. package/dist/tools/bio/get-drug-info.js +0 -22
  289. package/dist/tools/bio/get-mechanism.d.ts +0 -8
  290. package/dist/tools/bio/get-mechanism.js +0 -18
  291. package/dist/tools/bio/get-pubchem-assays.d.ts +0 -22
  292. package/dist/tools/bio/get-pubchem-assays.js +0 -107
  293. package/dist/tools/bio/get-pubchem-cross-refs.d.ts +0 -15
  294. package/dist/tools/bio/get-pubchem-cross-refs.js +0 -68
  295. package/dist/tools/bio/get-target-safety.d.ts +0 -18
  296. package/dist/tools/bio/get-target-safety.js +0 -26
  297. package/dist/tools/bio/search-compounds.d.ts +0 -22
  298. package/dist/tools/bio/search-compounds.js +0 -160
  299. package/dist/tools/bio/search-ctx-chemical.d.ts +0 -50
  300. package/dist/tools/bio/search-ctx-chemical.js +0 -130
  301. package/dist/tools/bio/search-ctx-exposure.d.ts +0 -55
  302. package/dist/tools/bio/search-ctx-exposure.js +0 -176
  303. package/dist/tools/bio/search-ctx-hazard.d.ts +0 -111
  304. package/dist/tools/bio/search-ctx-hazard.js +0 -178
  305. package/dist/tools/bio/search-opentargets.d.ts +0 -11
  306. package/dist/tools/bio/search-opentargets.js +0 -25
  307. package/dist/tools/bio/search-pubchem-compound.d.ts +0 -26
  308. package/dist/tools/bio/search-pubchem-compound.js +0 -110
  309. package/dist/tools/bio/search-pubmed.d.ts +0 -17
  310. package/dist/tools/bio/search-pubmed.js +0 -31
  311. package/dist/tools/bio/search-targets.d.ts +0 -9
  312. package/dist/tools/bio/search-targets.js +0 -20
  313. package/dist/tools/bio/search-toxcast.d.ts +0 -36
  314. package/dist/tools/bio/search-toxcast.js +0 -141
  315. package/dist/workflows/register-workflows.d.ts +0 -51
  316. package/dist/workflows/register-workflows.js +0 -48
  317. package/dist/workflows/resume-execute-analysis.d.ts +0 -70
  318. package/dist/workflows/resume-execute-analysis.js +0 -80
  319. package/dist/workflows/target-assessment/correction-loop.d.ts +0 -45
  320. package/dist/workflows/target-assessment/correction-loop.js +0 -62
  321. package/dist/workflows/target-assessment/steps/phase0-resolve.d.ts +0 -71
  322. package/dist/workflows/target-assessment/steps/phase0-resolve.js +0 -21
  323. package/dist/workflows/target-assessment/steps/phase1-aggregate.d.ts +0 -1018
  324. package/dist/workflows/target-assessment/steps/phase1-aggregate.js +0 -25
  325. package/dist/workspace/mount-strategy.d.ts +0 -50
  326. package/dist/workspace/mount-strategy.js +0 -67
  327. package/dist/workspace/provenance-collector.d.ts +0 -34
  328. package/dist/workspace/provenance-collector.js +0 -16
  329. package/dist/workspace/workspace-paths.d.ts +0 -26
  330. package/dist/workspace/workspace-paths.js +0 -31
@@ -0,0 +1,450 @@
1
+ /**
2
+ * EPA CompTox (CTX) operations — the four datasets the CompTox tools expose:
3
+ * ToxCast/Tox21 bioactivity, hazard (ToxValDB/ToxRefDB), chemical identity +
4
+ * physicochemical properties, and exposure.
5
+ *
6
+ * Every dataset keys off a DTXSID, so `resolveDtxsid` is the single entry
7
+ * point they share: a DTXSID is used directly, anything else is resolved by
8
+ * exact match against the chemical-search endpoint. `null` means the query
9
+ * resolved to no chemical — an expected outcome, not an error.
10
+ */
11
+ import { z } from "zod";
12
+ import { apiFetchValidated, describeApiError } from "./api-utils.js";
13
+ import { EPA_CCTE_BASE } from "./toxcast-config.js";
14
+ // The chemical-search endpoint returns rows the code reads `dtxsid` from
15
+ // unguarded, so `dtxsid` stays required — a row missing it is a contract break
16
+ // surfaced as `invalid_response`, not a silent `undefined`. The other fields
17
+ // are optional: the API omits absent values.
18
+ const CtxChemicalSearchRowSchema = z.object({
19
+ dtxsid: z.string(),
20
+ preferredName: z.string().nullable().optional(),
21
+ casrn: z.string().nullable().optional(),
22
+ });
23
+ /** Resolve a DTXSID / CASRN / name to a chemical. `null` = no match (valid no-data). */
24
+ export async function resolveDtxsid(query, headers) {
25
+ if (query.startsWith("DTXSID")) {
26
+ return { dtxsid: query, preferredName: query, casrn: null };
27
+ }
28
+ const url = `${EPA_CCTE_BASE}/chemical/search/equal/${encodeURIComponent(query)}`;
29
+ const res = await apiFetchValidated(url, z.array(CtxChemicalSearchRowSchema), { headers });
30
+ if (res.isErr())
31
+ throw new Error(describeApiError(res.error));
32
+ if (!res.value?.length)
33
+ return null;
34
+ const chem = res.value[0];
35
+ return {
36
+ dtxsid: chem.dtxsid,
37
+ preferredName: chem.preferredName ?? query,
38
+ casrn: chem.casrn ?? null,
39
+ };
40
+ }
41
+ // Raw CTX Bioactivity API wire shapes, validated at the fetch boundary. Fields
42
+ // are optional because the API omits absent values.
43
+ const ToxcastMc5ParamSchema = z.object({
44
+ ac50: z.number().optional(),
45
+ acc: z.number().optional(),
46
+ });
47
+ const ToxcastMc6ParamSchema = z.object({
48
+ flag: z.unknown().optional(),
49
+ });
50
+ const ToxcastBioactivityRowSchema = z.object({
51
+ aeid: z.number().optional(),
52
+ hitc: z.number().optional(),
53
+ maxMean: z.number().nullable().optional(),
54
+ modl: z.string().optional(),
55
+ mc5Param: ToxcastMc5ParamSchema.nullable().optional(),
56
+ mc6Param: ToxcastMc6ParamSchema.nullable().optional(),
57
+ });
58
+ const ToxcastAssaySummaryRowSchema = z.object({
59
+ aeid: z.number().optional(),
60
+ aenm: z.string().optional(),
61
+ });
62
+ /**
63
+ * ToxCast/Tox21 assay results for a chemical, sorted by AC50 ascending.
64
+ * `totalAssays` / `activeAssays` / `activeHitRate` always describe the full
65
+ * tested panel, never the returned slice.
66
+ */
67
+ export async function fetchToxcastBioactivity(dtxsid, headers, opts) {
68
+ const aeidMap = await fetchAssayNames(dtxsid, headers);
69
+ const bioUrl = `${EPA_CCTE_BASE}/bioactivity/data/search/by-dtxsid/${dtxsid}`;
70
+ const bioRes = await apiFetchValidated(bioUrl, z.array(ToxcastBioactivityRowSchema), { headers });
71
+ if (bioRes.isErr())
72
+ throw new Error(describeApiError(bioRes.error));
73
+ const allResults = bioRes.value ?? [];
74
+ const totalAssays = allResults.length;
75
+ const activeAssays = allResults.filter((r) => r.hitc === 1).length;
76
+ const filtered = opts.activeOnly ? allResults.filter((r) => r.hitc === 1) : allResults;
77
+ filtered.sort((a, b) => extractAc50(a) - extractAc50(b));
78
+ const results = filtered.slice(0, opts.limit).map((r) => {
79
+ const aeid = r.aeid ?? 0;
80
+ const mc6 = r.mc6Param ?? {};
81
+ const flags = Array.isArray(mc6.flag) ? mc6.flag : [];
82
+ return {
83
+ aeid,
84
+ assayEndpoint: aeidMap.get(aeid) ?? `aeid:${aeid}`,
85
+ ac50: extractAc50Raw(r),
86
+ hitCall: r.hitc ?? 0,
87
+ maxMean: r.maxMean ?? null,
88
+ model: r.modl ?? "",
89
+ flags,
90
+ };
91
+ });
92
+ return {
93
+ totalAssays,
94
+ activeAssays,
95
+ activeHitRate: totalAssays > 0 ? Math.round((activeAssays / totalAssays) * 1000) / 1000 : 0,
96
+ results,
97
+ };
98
+ }
99
+ function extractAc50(r) {
100
+ return extractAc50Raw(r) ?? Infinity;
101
+ }
102
+ function extractAc50Raw(r) {
103
+ const mc5 = r.mc5Param;
104
+ if (mc5 && typeof mc5 === "object") {
105
+ if (typeof mc5.ac50 === "number")
106
+ return mc5.ac50;
107
+ if (typeof mc5.acc === "number")
108
+ return mc5.acc;
109
+ }
110
+ return null;
111
+ }
112
+ async function fetchAssayNames(dtxsid, headers) {
113
+ const map = new Map();
114
+ const url = `${EPA_CCTE_BASE}/bioactivity/data/summary/search/by-dtxsid/${dtxsid}`;
115
+ const res = await apiFetchValidated(url, z.array(ToxcastAssaySummaryRowSchema), { headers });
116
+ if (res.isOk() && Array.isArray(res.value)) {
117
+ for (const s of res.value) {
118
+ if (s.aeid != null && s.aenm) {
119
+ map.set(s.aeid, s.aenm);
120
+ }
121
+ }
122
+ }
123
+ return map;
124
+ }
125
+ /* ---------------------------------- hazard ---------------------------------- */
126
+ // Each schema below both validates one raw CTX hazard row and normalizes it
127
+ // into the curated output shape via `.transform`; `z.infer` is that output
128
+ // type. Every wire field is optional (the API omits absent values); the two
129
+ // numeric fields the API sends as string-or-number stay `z.unknown()` so
130
+ // `toNumberOrNull` can coerce them without the schema rejecting the row.
131
+ const ToxValSchema = z
132
+ .object({
133
+ source: z.string().nullable().optional(),
134
+ toxvalType: z.string().nullable().optional(),
135
+ toxvalNumeric: z.unknown().optional(),
136
+ toxvalUnits: z.string().nullable().optional(),
137
+ studyType: z.string().nullable().optional(),
138
+ studyDurationClass: z.string().nullable().optional(),
139
+ speciesCommon: z.string().nullable().optional(),
140
+ exposureRoute: z.string().nullable().optional(),
141
+ toxicologicalEffect: z.string().nullable().optional(),
142
+ riskAssessmentClass: z.string().nullable().optional(),
143
+ humanEco: z.string().nullable().optional(),
144
+ year: z.unknown().optional(),
145
+ quality: z.string().nullable().optional(),
146
+ })
147
+ .transform((r) => ({
148
+ source: r.source ?? "",
149
+ toxvalType: r.toxvalType ?? "",
150
+ toxvalNumeric: toNumberOrNull(r.toxvalNumeric),
151
+ toxvalUnits: r.toxvalUnits ?? "",
152
+ studyType: r.studyType ?? "",
153
+ studyDurationClass: r.studyDurationClass ?? "",
154
+ species: r.speciesCommon ?? "",
155
+ exposureRoute: r.exposureRoute ?? "",
156
+ toxicologicalEffect: r.toxicologicalEffect ?? "",
157
+ riskAssessmentClass: r.riskAssessmentClass ?? "",
158
+ humanEco: r.humanEco ?? "",
159
+ year: toNumberOrNull(r.year),
160
+ quality: r.quality ?? "",
161
+ }));
162
+ const GenetoxSchema = z
163
+ .object({
164
+ source: z.string().nullable().optional(),
165
+ assayCategory: z.string().nullable().optional(),
166
+ assayType: z.string().nullable().optional(),
167
+ metabolicActivation: z.string().nullable().optional(),
168
+ species: z.string().nullable().optional(),
169
+ overallResult: z.string().nullable().optional(),
170
+ year: z.unknown().optional(),
171
+ })
172
+ .transform((r) => ({
173
+ source: r.source ?? "",
174
+ assayCategory: r.assayCategory ?? "",
175
+ assayType: r.assayType ?? "",
176
+ metabolicActivation: r.metabolicActivation ?? "",
177
+ species: r.species ?? "",
178
+ overallResult: r.overallResult ?? "",
179
+ year: toNumberOrNull(r.year),
180
+ }));
181
+ const CancerSchema = z
182
+ .object({
183
+ source: z.string().nullable().optional(),
184
+ classification: z.string().nullable().optional(),
185
+ cancerClassification: z.string().nullable().optional(),
186
+ url: z.string().nullable().optional(),
187
+ })
188
+ .transform((r) => ({
189
+ source: r.source ?? "",
190
+ classification: r.classification ?? r.cancerClassification ?? "",
191
+ url: r.url ?? "",
192
+ }));
193
+ /**
194
+ * In-vivo hazard data for a chemical. `limit` caps toxval and genetox only —
195
+ * cancer classifications always come back whole.
196
+ */
197
+ export async function fetchCtxHazardData(dtxsid, headers, opts) {
198
+ const { dataType, limit } = opts;
199
+ const data = {};
200
+ const fetchers = [];
201
+ if (dataType === "toxval" || dataType === "all") {
202
+ fetchers.push(fetchToxval(dtxsid, headers, limit).then((v) => {
203
+ data.toxval = v;
204
+ }));
205
+ }
206
+ if (dataType === "genetox" || dataType === "all") {
207
+ fetchers.push(fetchGenetox(dtxsid, headers, limit).then((v) => {
208
+ data.genetox = v;
209
+ }));
210
+ }
211
+ if (dataType === "cancer" || dataType === "all") {
212
+ fetchers.push(fetchCancer(dtxsid, headers).then((v) => {
213
+ data.cancer = v;
214
+ }));
215
+ }
216
+ await Promise.all(fetchers);
217
+ return data;
218
+ }
219
+ async function fetchToxval(dtxsid, headers, limit) {
220
+ const url = `${EPA_CCTE_BASE}/hazard/toxval/search/by-dtxsid/${dtxsid}`;
221
+ const res = await apiFetchValidated(url, z.array(ToxValSchema), { headers });
222
+ if (res.isErr() || !Array.isArray(res.value))
223
+ return [];
224
+ return res.value.slice(0, limit);
225
+ }
226
+ function toNumberOrNull(v) {
227
+ if (v == null || v === "")
228
+ return null;
229
+ const n = typeof v === "number" ? v : Number(v);
230
+ return Number.isFinite(n) ? n : null;
231
+ }
232
+ async function fetchGenetox(dtxsid, headers, limit) {
233
+ const url = `${EPA_CCTE_BASE}/hazard/genetox/summary/search/by-dtxsid/${dtxsid}`;
234
+ const res = await apiFetchValidated(url, z.array(GenetoxSchema), { headers });
235
+ if (res.isErr() || !Array.isArray(res.value))
236
+ return [];
237
+ return res.value.slice(0, limit);
238
+ }
239
+ async function fetchCancer(dtxsid, headers) {
240
+ const url = `${EPA_CCTE_BASE}/hazard/cancer-summary/search/by-dtxsid/${dtxsid}`;
241
+ const res = await apiFetchValidated(url, z.array(CancerSchema), { headers });
242
+ if (res.isErr() || !Array.isArray(res.value))
243
+ return [];
244
+ return res.value;
245
+ }
246
+ // Raw CTX chemical-detail wire shape, validated at the fetch boundary. Every
247
+ // field is optional (the API omits absent values); the mapper below normalizes
248
+ // it into `ChemicalDetail`, folding in the resolved `dtxsid` fallback.
249
+ const RawChemicalDetailSchema = z.object({
250
+ dtxsid: z.string().optional(),
251
+ dtxcid: z.string().nullable().optional(),
252
+ casrn: z.string().nullable().optional(),
253
+ preferredName: z.string().nullable().optional(),
254
+ iupacName: z.string().nullable().optional(),
255
+ molFormula: z.string().nullable().optional(),
256
+ smiles: z.string().nullable().optional(),
257
+ inchikey: z.string().nullable().optional(),
258
+ monoisotopicMass: z.number().nullable().optional(),
259
+ averageMass: z.number().nullable().optional(),
260
+ qcLevel: z.number().nullable().optional(),
261
+ totalAssays: z.number().nullable().optional(),
262
+ activeAssays: z.number().nullable().optional(),
263
+ percentAssays: z.number().nullable().optional(),
264
+ pubchemCid: z.number().nullable().optional(),
265
+ pubmedCount: z.number().nullable().optional(),
266
+ sourcesCount: z.number().nullable().optional(),
267
+ });
268
+ const RawPropertySummarySchema = z.object({
269
+ propName: z.string().nullable().optional(),
270
+ unit: z.string().nullable().optional(),
271
+ experimentalCount: z.number().nullable().optional(),
272
+ experimentalMedian: z.number().nullable().optional(),
273
+ experimentalMin: z.number().nullable().optional(),
274
+ experimentalMax: z.number().nullable().optional(),
275
+ predictedCount: z.number().nullable().optional(),
276
+ predictedMedian: z.number().nullable().optional(),
277
+ predictedMin: z.number().nullable().optional(),
278
+ predictedMax: z.number().nullable().optional(),
279
+ });
280
+ /** Chemical identity, mass, and dashboard counts for a DTXSID. */
281
+ export async function fetchCtxChemicalDetail(dtxsid, headers) {
282
+ const detailUrl = `${EPA_CCTE_BASE}/chemical/detail/search/by-dtxsid/${dtxsid}?projection=chemicaldetailstandard`;
283
+ const detailRes = await apiFetchValidated(detailUrl, RawChemicalDetailSchema, { headers });
284
+ if (detailRes.isErr())
285
+ throw new Error(describeApiError(detailRes.error));
286
+ const d = detailRes.value ?? {};
287
+ return {
288
+ dtxsid: d.dtxsid ?? dtxsid,
289
+ dtxcid: d.dtxcid ?? "",
290
+ casrn: d.casrn ?? null,
291
+ preferredName: d.preferredName ?? "",
292
+ iupacName: d.iupacName ?? "",
293
+ molFormula: d.molFormula ?? "",
294
+ smiles: d.smiles ?? "",
295
+ inchikey: d.inchikey ?? "",
296
+ monoisotopicMass: d.monoisotopicMass ?? null,
297
+ averageMass: d.averageMass ?? null,
298
+ qcLevel: d.qcLevel ?? null,
299
+ totalAssays: d.totalAssays ?? null,
300
+ activeAssays: d.activeAssays ?? null,
301
+ percentAssays: d.percentAssays ?? null,
302
+ pubchemCid: d.pubchemCid ?? null,
303
+ pubmedCount: d.pubmedCount ?? null,
304
+ sourcesCount: d.sourcesCount ?? null,
305
+ };
306
+ }
307
+ /** Experimental + predicted physicochemical property summaries. `undefined` = none served. */
308
+ export async function fetchCtxChemicalProperties(dtxsid, headers) {
309
+ const propUrl = `${EPA_CCTE_BASE}/chemical/property/summary/search/by-dtxsid/${dtxsid}`;
310
+ const propRes = await apiFetchValidated(propUrl, z.array(RawPropertySummarySchema), { headers });
311
+ if (propRes.isOk() && Array.isArray(propRes.value)) {
312
+ return propRes.value.map((p) => ({
313
+ propName: p.propName ?? "",
314
+ unit: p.unit ?? "",
315
+ experimentalCount: p.experimentalCount ?? null,
316
+ experimentalMedian: p.experimentalMedian ?? null,
317
+ experimentalMin: p.experimentalMin ?? null,
318
+ experimentalMax: p.experimentalMax ?? null,
319
+ predictedCount: p.predictedCount ?? null,
320
+ predictedMedian: p.predictedMedian ?? null,
321
+ predictedMin: p.predictedMin ?? null,
322
+ predictedMax: p.predictedMax ?? null,
323
+ }));
324
+ }
325
+ return undefined;
326
+ }
327
+ // SEEM exposure-prediction wire shape (endpoint returns a single object or an
328
+ // array). `probabilityPesticde` is the API's own misspelling, read as a
329
+ // fallback before the corrected `probabilityPesticide`, so both stay modeled.
330
+ const RawSeemPredictionSchema = z.object({
331
+ dtxsid: z.string().optional(),
332
+ productionVolume: z.number().nullable().optional(),
333
+ units: z.string().nullable().optional(),
334
+ probabilityDietary: z.number().nullable().optional(),
335
+ probabilityResidential: z.number().nullable().optional(),
336
+ probabilityPesticde: z.number().nullable().optional(),
337
+ probabilityPesticide: z.number().nullable().optional(),
338
+ probabilityIndustrial: z.number().nullable().optional(),
339
+ });
340
+ const RawHttkRowSchema = z.object({
341
+ parameter: z.string().nullable().optional(),
342
+ measured: z.number().nullable().optional(),
343
+ predicted: z.number().nullable().optional(),
344
+ units: z.string().nullable().optional(),
345
+ model: z.string().nullable().optional(),
346
+ species: z.string().nullable().optional(),
347
+ reference: z.string().nullable().optional(),
348
+ });
349
+ const RawFunctionalUseRowSchema = z.object({
350
+ functioncategory: z.string().nullable().optional(),
351
+ reportedfunction: z.string().nullable().optional(),
352
+ doctitle: z.string().nullable().optional(),
353
+ });
354
+ const RawProductDataRowSchema = z.object({
355
+ productname: z.string().nullable().optional(),
356
+ gencat: z.string().nullable().optional(),
357
+ prodfam: z.string().nullable().optional(),
358
+ prodtype: z.string().nullable().optional(),
359
+ centralweightfraction: z.number().nullable().optional(),
360
+ weightfractiontype: z.string().nullable().optional(),
361
+ });
362
+ /**
363
+ * Human-exposure data for a chemical. `limit` caps functional-use and
364
+ * product-data only — seem and httk always come back whole.
365
+ */
366
+ export async function fetchCtxExposureData(dtxsid, headers, opts) {
367
+ const { dataType, limit } = opts;
368
+ const data = {};
369
+ const fetchers = [];
370
+ if (dataType === "seem" || dataType === "all") {
371
+ fetchers.push(fetchSeem(dtxsid, headers).then((v) => {
372
+ data.seem = v;
373
+ }));
374
+ }
375
+ if (dataType === "httk" || dataType === "all") {
376
+ fetchers.push(fetchHttk(dtxsid, headers).then((v) => {
377
+ data.httk = v;
378
+ }));
379
+ }
380
+ if (dataType === "functional-use" || dataType === "all") {
381
+ fetchers.push(fetchFunctionalUse(dtxsid, headers, limit).then((v) => {
382
+ data.functionalUse = v;
383
+ }));
384
+ }
385
+ if (dataType === "product-data" || dataType === "all") {
386
+ fetchers.push(fetchProductData(dtxsid, headers, limit).then((v) => {
387
+ data.productData = v;
388
+ }));
389
+ }
390
+ await Promise.all(fetchers);
391
+ return data;
392
+ }
393
+ async function fetchSeem(dtxsid, headers) {
394
+ const url = `${EPA_CCTE_BASE}/exposure/seem/general/search/by-dtxsid/${dtxsid}`;
395
+ const res = await apiFetchValidated(url, z.union([RawSeemPredictionSchema, z.array(RawSeemPredictionSchema)]), { headers });
396
+ if (res.isErr() || !res.value)
397
+ return undefined;
398
+ const d = Array.isArray(res.value) ? res.value[0] : res.value;
399
+ if (!d)
400
+ return undefined;
401
+ return {
402
+ dtxsid: d.dtxsid ?? dtxsid,
403
+ productionVolume: d.productionVolume ?? null,
404
+ units: d.units ?? "",
405
+ probabilityDietary: d.probabilityDietary ?? null,
406
+ probabilityResidential: d.probabilityResidential ?? null,
407
+ probabilityPesticide: d.probabilityPesticde ?? d.probabilityPesticide ?? null,
408
+ probabilityIndustrial: d.probabilityIndustrial ?? null,
409
+ };
410
+ }
411
+ async function fetchHttk(dtxsid, headers) {
412
+ const url = `${EPA_CCTE_BASE}/exposure/httk/search/by-dtxsid/${dtxsid}`;
413
+ const res = await apiFetchValidated(url, z.array(RawHttkRowSchema), { headers });
414
+ if (res.isErr() || !Array.isArray(res.value))
415
+ return [];
416
+ return res.value.map((r) => ({
417
+ parameter: r.parameter ?? "",
418
+ measured: r.measured ?? null,
419
+ predicted: r.predicted ?? null,
420
+ units: r.units ?? "",
421
+ model: r.model ?? "",
422
+ species: r.species ?? "",
423
+ reference: r.reference ?? "",
424
+ }));
425
+ }
426
+ async function fetchFunctionalUse(dtxsid, headers, limit) {
427
+ const url = `${EPA_CCTE_BASE}/exposure/functional-use/search/by-dtxsid/${dtxsid}`;
428
+ const res = await apiFetchValidated(url, z.array(RawFunctionalUseRowSchema), { headers });
429
+ if (res.isErr() || !Array.isArray(res.value))
430
+ return [];
431
+ return res.value.slice(0, limit).map((r) => ({
432
+ functionCategory: r.functioncategory ?? "",
433
+ reportedFunction: r.reportedfunction ?? "",
434
+ docTitle: r.doctitle ?? "",
435
+ }));
436
+ }
437
+ async function fetchProductData(dtxsid, headers, limit) {
438
+ const url = `${EPA_CCTE_BASE}/exposure/product-data/search/by-dtxsid/${dtxsid}`;
439
+ const res = await apiFetchValidated(url, z.array(RawProductDataRowSchema), { headers });
440
+ if (res.isErr() || !Array.isArray(res.value))
441
+ return [];
442
+ return res.value.slice(0, limit).map((r) => ({
443
+ productName: r.productname ?? "",
444
+ generalCategory: r.gencat ?? "",
445
+ productFamily: r.prodfam ?? "",
446
+ productType: r.prodtype ?? "",
447
+ centralWeightFraction: r.centralweightfraction ?? null,
448
+ weightFractionType: r.weightfractiontype ?? "",
449
+ }));
450
+ }
@@ -28,7 +28,16 @@ const IupharTargetSchema = z.object({
28
28
  });
29
29
  const UNIPROT_ACCESSION_RE = /^[A-Z][0-9][A-Z0-9]{3}[0-9]$/;
30
30
  function stripHtml(name) {
31
- return name.replace(/<[^>]+>/g, "");
31
+ // Strip tags to a fixpoint: a single pass can leave a `<…>` behind when
32
+ // removing an inner tag lets the surrounding angle brackets form a new one,
33
+ // so repeat until the string stops changing.
34
+ let out = name;
35
+ let prev;
36
+ do {
37
+ prev = out;
38
+ out = out.replace(/<[^>]+>/g, "");
39
+ } while (out !== prev);
40
+ return out;
32
41
  }
33
42
  function notFound(error) {
34
43
  return error.type === "http_status" && (error.status === 404 || error.status === 204);
@@ -34,8 +34,16 @@ const OpenFdaLabelSchema = z.object({
34
34
  rems_indication: z.unknown().optional(),
35
35
  medication_guide: z.unknown().optional(),
36
36
  });
37
+ /**
38
+ * Escape a value for embedding inside a double-quoted openFDA (Lucene) phrase.
39
+ * The backslash is escaped first so a caller-supplied `\` cannot smuggle an
40
+ * unescaped `"` past the quote escaping and break out of the phrase.
41
+ */
42
+ function escapeQueryPhrase(value) {
43
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
44
+ }
37
45
  function buildSearch(drugName, serious) {
38
- const escaped = drugName.replace(/"/g, '\\"');
46
+ const escaped = escapeQueryPhrase(drugName);
39
47
  let search = `patient.drug.openfda.generic_name:"${escaped}"`;
40
48
  if (serious)
41
49
  search += "+AND+serious:1";
@@ -68,7 +76,7 @@ export async function getFaersByDrug(drugName, options = {}) {
68
76
  * and §2.6.8 risk summary.
69
77
  */
70
78
  export async function getFaersSeriousness(drugName) {
71
- const escaped = drugName.replace(/"/g, '\\"');
79
+ const escaped = escapeQueryPhrase(drugName);
72
80
  const search = `patient.drug.openfda.generic_name:"${escaped}"`;
73
81
  const totalUrl = `${OPENFDA_BASE}?search=${encodeURIComponent(search)}&limit=1`;
74
82
  const totalRes = await apiFetchValidated(totalUrl, FaersMetaResponseSchema);
@@ -179,7 +187,7 @@ const OpenFdaLabelResponseSchema = z.object({
179
187
  */
180
188
  export async function getDrugLabelActions(genericName, options = {}) {
181
189
  const limit = options.limit ?? 5;
182
- const escaped = genericName.replace(/"/g, '\\"');
190
+ const escaped = escapeQueryPhrase(genericName);
183
191
  const search = `openfda.generic_name:"${escaped}"`;
184
192
  const url = `${OPENFDA_LABEL_BASE}?search=${encodeURIComponent(search)}` + `&sort=effective_time:desc&limit=${limit}`;
185
193
  const res = await apiFetchValidated(url, OpenFdaLabelResponseSchema);
@@ -8,10 +8,16 @@ import { apiFetch, apiFetchValidated, parseTSV } from "./api-utils.js";
8
8
  const KEGG_BASE = "https://rest.kegg.jp";
9
9
  const REACTOME_BASE = "https://reactome.org/ContentService";
10
10
  export function stripHtmlAndCollapseWs(s) {
11
- return s
12
- .replace(/<[^>]+>/g, "")
13
- .replace(/\s+/g, " ")
14
- .trim();
11
+ // Strip tags to a fixpoint: a single pass can leave a `<…>` behind when
12
+ // removing an inner tag lets the surrounding angle brackets form a new one,
13
+ // so repeat until the string stops changing.
14
+ let out = s;
15
+ let prev;
16
+ do {
17
+ prev = out;
18
+ out = out.replace(/<[^>]+>/g, "");
19
+ } while (out !== prev);
20
+ return out.replace(/\s+/g, " ").trim();
15
21
  }
16
22
  const ORGANISM_MAP = {
17
23
  hsa: "Homo sapiens",
@@ -0,0 +1,74 @@
1
+ /**
2
+ * PubChem PUG-REST operations — the three lookups the PubChem tools expose.
3
+ *
4
+ * One home for the wire schemas, the URL construction, and the "PubChem 404
5
+ * means no data" contract, so a caller cannot drift from it: every function
6
+ * returns an empty collection for a 404 and throws on any other API failure.
7
+ */
8
+ import { z } from "zod";
9
+ export type PubChemSearchBy = "name" | "smiles" | "inchi" | "inchikey" | "cid";
10
+ declare const PugPropertyRowSchema: z.ZodPipe<z.ZodObject<{
11
+ CID: z.ZodOptional<z.ZodNumber>;
12
+ CanonicalSMILES: z.ZodOptional<z.ZodString>;
13
+ MolecularFormula: z.ZodOptional<z.ZodString>;
14
+ MolecularWeight: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
15
+ IUPACName: z.ZodOptional<z.ZodString>;
16
+ InChI: z.ZodOptional<z.ZodString>;
17
+ InChIKey: z.ZodOptional<z.ZodString>;
18
+ XLogP: z.ZodOptional<z.ZodNumber>;
19
+ TPSA: z.ZodOptional<z.ZodNumber>;
20
+ HBondDonorCount: z.ZodOptional<z.ZodNumber>;
21
+ HBondAcceptorCount: z.ZodOptional<z.ZodNumber>;
22
+ RotatableBondCount: z.ZodOptional<z.ZodNumber>;
23
+ Complexity: z.ZodOptional<z.ZodNumber>;
24
+ }, z.core.$strip>, z.ZodTransform<{
25
+ cid: number;
26
+ canonicalSmiles: string | null;
27
+ molecularFormula: string | null;
28
+ molecularWeight: number | null;
29
+ iupacName: string | null;
30
+ inchi: string | null;
31
+ inchiKey: string | null;
32
+ xlogp: number | null;
33
+ tpsa: number | null;
34
+ hbondDonorCount: number | null;
35
+ hbondAcceptorCount: number | null;
36
+ rotatableBondCount: number | null;
37
+ complexity: number | null;
38
+ }, {
39
+ CID?: number | undefined;
40
+ CanonicalSMILES?: string | undefined;
41
+ MolecularFormula?: string | undefined;
42
+ MolecularWeight?: string | number | undefined;
43
+ IUPACName?: string | undefined;
44
+ InChI?: string | undefined;
45
+ InChIKey?: string | undefined;
46
+ XLogP?: number | undefined;
47
+ TPSA?: number | undefined;
48
+ HBondDonorCount?: number | undefined;
49
+ HBondAcceptorCount?: number | undefined;
50
+ RotatableBondCount?: number | undefined;
51
+ Complexity?: number | undefined;
52
+ }>>;
53
+ export type PubChemCompound = z.infer<typeof PugPropertyRowSchema>;
54
+ export interface PubChemCrossRef {
55
+ source: string;
56
+ id: string;
57
+ }
58
+ export interface PubChemAssay {
59
+ aid: number;
60
+ assayName: string | null;
61
+ targetName: string | null;
62
+ activityOutcome: string | null;
63
+ activityValue: number | null;
64
+ }
65
+ /** Resolve a compound to its identity + computed properties. Empty array = no match. */
66
+ export declare function fetchPubchemCompounds(query: string, searchBy: PubChemSearchBy): Promise<PubChemCompound[]>;
67
+ /** External registry ids for a CID. Empty array = the CID is in no external registry. */
68
+ export declare function fetchPubchemCrossRefs(cid: number): Promise<PubChemCrossRef[]>;
69
+ /** Bioassay screening summaries for a CID. Empty array = never screened. */
70
+ export declare function fetchPubchemAssays(cid: number, opts: {
71
+ activeOnly: boolean;
72
+ limit: number;
73
+ }): Promise<PubChemAssay[]>;
74
+ export {};