@inflexa-ai/harness 0.30.2 → 0.31.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/contracts/target-dossier.d.ts +61 -61
- package/dist/execution/run-synthesis.js +7 -2
- package/dist/lib/zod-issues.d.ts +8 -0
- package/dist/lib/zod-issues.js +13 -0
- package/dist/literature/sources/semantic-scholar.d.ts +34 -0
- package/dist/literature/sources/semantic-scholar.js +15 -4
- package/dist/sandbox/cpu-files.d.ts +37 -0
- package/dist/sandbox/cpu-files.js +55 -0
- package/dist/sandbox/docker-client.d.ts +15 -0
- package/dist/sandbox/docker-client.js +100 -11
- package/dist/sandbox/k8s-client.d.ts +5 -0
- package/dist/sandbox/k8s-client.js +128 -19
- package/dist/sandbox/thread-env.d.ts +29 -0
- package/dist/sandbox/thread-env.js +49 -0
- package/dist/state/data-profile.d.ts +35 -0
- package/dist/state/data-profile.js +56 -0
- package/dist/state/index.d.ts +1 -1
- package/dist/state/index.js +1 -1
- package/dist/tasks/data-profile.d.ts +16 -1
- package/dist/tasks/data-profile.js +31 -10
- package/dist/tools/bio/chembl.js +4 -3
- package/dist/tools/bio/drug-gene-interactions.d.ts +2 -14
- package/dist/tools/bio/drug-gene-interactions.js +10 -18
- package/dist/tools/bio/gene-disease-evidence.d.ts +3 -1
- package/dist/tools/bio/gene-disease-evidence.js +12 -9
- package/dist/tools/bio/keys.js +1 -0
- package/dist/tools/bio/lookup-annotation.d.ts +43 -0
- package/dist/tools/bio/lookup-annotation.js +19 -4
- package/dist/tools/bio/pubchem.js +5 -3
- package/dist/tools/bio/search-clinical-trials.js +5 -4
- package/dist/tools/bio/search-geo-datasets.d.ts +39 -9
- package/dist/tools/bio/search-geo-datasets.js +38 -20
- package/dist/tools/lib/api-utils.d.ts +27 -2
- package/dist/tools/lib/api-utils.js +43 -3
- package/dist/tools/lib/bgee-client.d.ts +16 -0
- package/dist/tools/lib/bgee-client.js +16 -5
- package/dist/tools/lib/cbioportal-client.d.ts +20 -0
- package/dist/tools/lib/cbioportal-client.js +16 -8
- package/dist/tools/lib/chembl-client.d.ts +105 -3
- package/dist/tools/lib/chembl-client.js +196 -84
- package/dist/tools/lib/clinical-benchmarks-client.d.ts +5 -0
- package/dist/tools/lib/clinical-benchmarks-client.js +5 -0
- package/dist/tools/lib/clinical-trials-client.d.ts +131 -2
- package/dist/tools/lib/clinical-trials-client.js +32 -6
- package/dist/tools/lib/clinvar-client.d.ts +35 -0
- package/dist/tools/lib/clinvar-client.js +34 -28
- package/dist/tools/lib/ctx-ops.d.ts +106 -35
- package/dist/tools/lib/ctx-ops.js +135 -69
- package/dist/tools/lib/dgidb-client.d.ts +73 -0
- package/dist/tools/lib/dgidb-client.js +25 -13
- package/dist/tools/lib/disgenet-client.d.ts +117 -24
- package/dist/tools/lib/disgenet-client.js +127 -34
- package/dist/tools/lib/disgenet-config.d.ts +1 -1
- package/dist/tools/lib/disgenet-config.js +5 -3
- package/dist/tools/lib/drugbank-client.d.ts +57 -58
- package/dist/tools/lib/drugbank-client.js +69 -64
- package/dist/tools/lib/drugbank-config.d.ts +6 -1
- package/dist/tools/lib/drugbank-config.js +6 -1
- package/dist/tools/lib/ema-client.d.ts +53 -0
- package/dist/tools/lib/ema-client.js +19 -7
- package/dist/tools/lib/ensembl-client.d.ts +4 -0
- package/dist/tools/lib/ensembl-client.js +4 -0
- package/dist/tools/lib/gwas-catalog-client.d.ts +64 -1
- package/dist/tools/lib/gwas-catalog-client.js +21 -6
- package/dist/tools/lib/identifier-resolver.d.ts +17 -1
- package/dist/tools/lib/identifier-resolver.js +13 -4
- package/dist/tools/lib/impc-client.d.ts +4 -0
- package/dist/tools/lib/impc-client.js +4 -0
- package/dist/tools/lib/iuphar-client.d.ts +10 -5
- package/dist/tools/lib/iuphar-client.js +49 -13
- package/dist/tools/lib/monarch-client.d.ts +4 -0
- package/dist/tools/lib/monarch-client.js +4 -0
- package/dist/tools/lib/openfda-client.d.ts +34 -0
- package/dist/tools/lib/openfda-client.js +22 -8
- package/dist/tools/lib/opentargets-client.d.ts +99 -4
- package/dist/tools/lib/opentargets-client.js +97 -37
- package/dist/tools/lib/pathway-client.d.ts +45 -0
- package/dist/tools/lib/pathway-client.js +88 -8
- package/dist/tools/lib/pharmgkb-client.d.ts +35 -1
- package/dist/tools/lib/pharmgkb-client.js +22 -6
- package/dist/tools/lib/pharmgkb-config.d.ts +8 -1
- package/dist/tools/lib/pharmgkb-config.js +8 -1
- package/dist/tools/lib/pubchem-client.d.ts +22 -0
- package/dist/tools/lib/pubchem-client.js +36 -33
- package/dist/tools/lib/pubchem-ops.d.ts +85 -5
- package/dist/tools/lib/pubchem-ops.js +96 -68
- package/dist/tools/lib/pubmed-client.d.ts +4 -0
- package/dist/tools/lib/pubmed-client.js +4 -0
- package/dist/tools/lib/string-client.d.ts +52 -0
- package/dist/tools/lib/string-client.js +12 -7
- package/dist/tools/lib/uniprot-client.d.ts +4 -0
- package/dist/tools/lib/uniprot-client.js +4 -0
- package/dist/tools/research/context7-docs.d.ts +23 -0
- package/dist/tools/research/context7-docs.js +49 -9
- package/dist/tools/research/search-github-repos.d.ts +4 -0
- package/dist/tools/research/search-github-repos.js +4 -0
- package/dist/workflows/execute-target-assessment.js +4 -4
- package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +2 -1
- package/dist/workflows/target-assessment/assemblers/orchestrator.js +3 -3
- package/dist/workflows/target-assessment/collectors/index.d.ts +8 -2
- package/dist/workflows/target-assessment/collectors/index.js +37 -10
- package/dist/workflows/target-assessment/fanout/index.d.ts +2 -1
- package/dist/workflows/target-assessment/fanout/index.js +2 -2
- package/dist/workflows/target-assessment/lib/safety-corroboration.js +2 -2
- package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +3 -2
- package/dist/workflows/target-assessment/lib/target-identity-filter.js +16 -5
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +61 -60
- package/dist/workflows/target-assessment/phase4-assemble.js +2 -2
- package/dist/workflows/target-assessment/schemas.d.ts +2 -8
- package/dist/workflows/target-assessment/schemas.js +8 -2
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +1 -4
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -4
- package/package.json +1 -1
- package/dist/agents/sandbox/__fixtures__/deps.d.ts +0 -23
- package/dist/agents/sandbox/__fixtures__/deps.js +0 -102
- package/dist/citations/__fixtures__/resolver.d.ts +0 -3
- package/dist/citations/__fixtures__/resolver.js +0 -9
- package/dist/loop/__fixtures__/scripted-provider.d.ts +0 -31
- package/dist/loop/__fixtures__/scripted-provider.js +0 -81
- package/dist/providers/__fixtures__/session.d.ts +0 -17
- package/dist/providers/__fixtures__/session.js +0 -20
- package/dist/tools/__fixtures__/tool-context.d.ts +0 -13
- package/dist/tools/__fixtures__/tool-context.js +0 -22
|
@@ -7,19 +7,53 @@
|
|
|
7
7
|
* point they share: a DTXSID is used directly, anything else is resolved by
|
|
8
8
|
* exact match against the chemical-search endpoint. `null` means the query
|
|
9
9
|
* resolved to no chemical — an expected outcome, not an error.
|
|
10
|
+
*
|
|
11
|
+
* UNVERIFIED CONTRACT, 2026-08-28. Every data path of the CTX API demands
|
|
12
|
+
* `EPA_CCTE_API_KEY`, and no key exists, thus no schema here parsed a live
|
|
13
|
+
* answer. The contract comes from three secondary sources: the server source of
|
|
14
|
+
* the EPA (`github.com/USEPA/ccte-api-{chemical,hazard,exposure,bioactivity}`,
|
|
15
|
+
* branch `dev`), the contract tests of the EPA (`USEPA/ccte-api-karate`), and
|
|
16
|
+
* the response bodies that the R client of the EPA (`ctxR` 1.1.3) recorded
|
|
17
|
+
* against this same base. A Java `List<T>` return type serializes to a JSON
|
|
18
|
+
* array, thus the server source settles each container shape.
|
|
19
|
+
*
|
|
20
|
+
* Absence policy: the CTX API sends an explicit `null` for an absent value, and
|
|
21
|
+
* it omits a key that its projection excludes. Thus a field carries both
|
|
22
|
+
* `.nullable()` and `.optional()`.
|
|
10
23
|
*/
|
|
11
24
|
import { z } from "zod";
|
|
12
|
-
import { apiFetchValidated, describeApiError } from "./api-utils.js";
|
|
25
|
+
import { apiFetchValidated, describeApiError, zWireNumber } from "./api-utils.js";
|
|
13
26
|
import { EPA_CCTE_BASE } from "./toxcast-config.js";
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
dtxsid: z.string(),
|
|
27
|
+
// A row of the chemical-search endpoint. `dtxsid` is nullable, because a search
|
|
28
|
+
// that matches a DTXCID or a Markush record answers with a row that carries no
|
|
29
|
+
// substance identifier. The caller drops such a row.
|
|
30
|
+
export const CtxChemicalSearchRowSchema = z.object({
|
|
31
|
+
dtxsid: z.string().nullable().optional(),
|
|
20
32
|
preferredName: z.string().nullable().optional(),
|
|
21
33
|
casrn: z.string().nullable().optional(),
|
|
22
34
|
});
|
|
35
|
+
/**
|
|
36
|
+
* Is the error the RFC 7807 answer of a search that matched nothing?
|
|
37
|
+
*
|
|
38
|
+
* The chemical-search endpoint never answers with an empty array. It answers
|
|
39
|
+
* HTTP 400 with a `ProblemDetail` body that carries `title`, `detail`, and a
|
|
40
|
+
* `suggestions` list. Thus that one 400 is the no-match outcome, and every other
|
|
41
|
+
* 4xx stays a failure.
|
|
42
|
+
*/
|
|
43
|
+
function isNoMatchProblemDetail(error) {
|
|
44
|
+
if (error.type !== "http_status" || error.status !== 400)
|
|
45
|
+
return false;
|
|
46
|
+
try {
|
|
47
|
+
const body = JSON.parse(error.body);
|
|
48
|
+
// `suggestions` separates the no-match answer from a generic 400
|
|
49
|
+
// `ProblemDetail`, which carries `title` and `detail` alone. Without it,
|
|
50
|
+
// a malformed request reads as a clean no-match.
|
|
51
|
+
return typeof body === "object" && body !== null && "detail" in body && "title" in body && "suggestions" in body;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
23
57
|
/** Resolve a DTXSID / CASRN / name to a chemical. `null` = no match (valid no-data). */
|
|
24
58
|
export async function resolveDtxsid(query, headers) {
|
|
25
59
|
if (query.startsWith("DTXSID")) {
|
|
@@ -27,11 +61,14 @@ export async function resolveDtxsid(query, headers) {
|
|
|
27
61
|
}
|
|
28
62
|
const url = `${EPA_CCTE_BASE}/chemical/search/equal/${encodeURIComponent(query)}`;
|
|
29
63
|
const res = await apiFetchValidated(url, z.array(CtxChemicalSearchRowSchema), { headers });
|
|
30
|
-
if (res.isErr())
|
|
64
|
+
if (res.isErr()) {
|
|
65
|
+
if (isNoMatchProblemDetail(res.error))
|
|
66
|
+
return null;
|
|
31
67
|
throw new Error(describeApiError(res.error));
|
|
32
|
-
|
|
68
|
+
}
|
|
69
|
+
const chem = (res.value ?? []).find((row) => row.dtxsid);
|
|
70
|
+
if (!chem?.dtxsid)
|
|
33
71
|
return null;
|
|
34
|
-
const chem = res.value[0];
|
|
35
72
|
return {
|
|
36
73
|
dtxsid: chem.dtxsid,
|
|
37
74
|
preferredName: chem.preferredName ?? query,
|
|
@@ -47,7 +84,7 @@ const ToxcastMc5ParamSchema = z.object({
|
|
|
47
84
|
const ToxcastMc6ParamSchema = z.object({
|
|
48
85
|
flag: z.unknown().optional(),
|
|
49
86
|
});
|
|
50
|
-
const ToxcastBioactivityRowSchema = z.object({
|
|
87
|
+
export const ToxcastBioactivityRowSchema = z.object({
|
|
51
88
|
aeid: z.number().optional(),
|
|
52
89
|
hitc: z.number().optional(),
|
|
53
90
|
maxMean: z.number().nullable().optional(),
|
|
@@ -55,27 +92,40 @@ const ToxcastBioactivityRowSchema = z.object({
|
|
|
55
92
|
mc5Param: ToxcastMc5ParamSchema.nullable().optional(),
|
|
56
93
|
mc6Param: ToxcastMc6ParamSchema.nullable().optional(),
|
|
57
94
|
});
|
|
58
|
-
|
|
95
|
+
/** One assay annotation of `bioactivity/assay/search/by-aeid/`. */
|
|
96
|
+
const ToxcastAssayAnnotationSchema = z.object({
|
|
59
97
|
aeid: z.number().optional(),
|
|
60
|
-
|
|
98
|
+
assayComponentEndpointName: z.string().nullable().optional(),
|
|
61
99
|
});
|
|
100
|
+
/**
|
|
101
|
+
* The `hitc` of invitrodb v4 is a continuous value from 0 to 1, not a flag. The
|
|
102
|
+
* EPA calls a value of 0.9 or more active, thus the threshold is the predicate
|
|
103
|
+
* and an equality against 1 counts almost nothing.
|
|
104
|
+
*/
|
|
105
|
+
const ACTIVE_HIT_CALL = 0.9;
|
|
106
|
+
function isActive(row) {
|
|
107
|
+
return (row.hitc ?? 0) >= ACTIVE_HIT_CALL;
|
|
108
|
+
}
|
|
62
109
|
/**
|
|
63
110
|
* ToxCast/Tox21 assay results for a chemical, sorted by AC50 ascending.
|
|
64
111
|
* `totalAssays` / `activeAssays` / `activeHitRate` always describe the full
|
|
65
112
|
* tested panel, never the returned slice.
|
|
66
113
|
*/
|
|
67
114
|
export async function fetchToxcastBioactivity(dtxsid, headers, opts) {
|
|
68
|
-
const aeidMap = await fetchAssayNames(dtxsid, headers);
|
|
69
115
|
const bioUrl = `${EPA_CCTE_BASE}/bioactivity/data/search/by-dtxsid/${dtxsid}`;
|
|
70
116
|
const bioRes = await apiFetchValidated(bioUrl, z.array(ToxcastBioactivityRowSchema), { headers });
|
|
71
117
|
if (bioRes.isErr())
|
|
72
118
|
throw new Error(describeApiError(bioRes.error));
|
|
73
119
|
const allResults = bioRes.value ?? [];
|
|
74
120
|
const totalAssays = allResults.length;
|
|
75
|
-
const activeAssays = allResults.filter(
|
|
76
|
-
const filtered = opts.activeOnly ? allResults.filter(
|
|
121
|
+
const activeAssays = allResults.filter(isActive).length;
|
|
122
|
+
const filtered = opts.activeOnly ? allResults.filter(isActive) : allResults;
|
|
77
123
|
filtered.sort((a, b) => extractAc50(a) - extractAc50(b));
|
|
78
|
-
const
|
|
124
|
+
const page = filtered.slice(0, opts.limit);
|
|
125
|
+
// The names come after the slice, thus one batch call covers the rows that
|
|
126
|
+
// the caller sees and never the whole tested panel.
|
|
127
|
+
const aeidMap = await fetchAssayNames(page, headers);
|
|
128
|
+
const results = page.map((r) => {
|
|
79
129
|
const aeid = r.aeid ?? 0;
|
|
80
130
|
const mc6 = r.mc6Param ?? {};
|
|
81
131
|
const flags = Array.isArray(mc6.flag) ? mc6.flag : [];
|
|
@@ -109,14 +159,31 @@ function extractAc50Raw(r) {
|
|
|
109
159
|
}
|
|
110
160
|
return null;
|
|
111
161
|
}
|
|
112
|
-
|
|
162
|
+
/**
|
|
163
|
+
* The endpoint name of each assay of a result page, keyed by its aeid.
|
|
164
|
+
*
|
|
165
|
+
* The per-chemical summary endpoint serves a `ChemicalAgg` roll-up, which
|
|
166
|
+
* carries no assay identifier and no assay name. The names live on the assay
|
|
167
|
+
* resource, and its batch route takes the whole aeid list in one POST body.
|
|
168
|
+
* Thus a page of results costs one request, not one for each row.
|
|
169
|
+
*
|
|
170
|
+
* A failure gives an empty map, and the caller then labels a row `aeid:N`.
|
|
171
|
+
*/
|
|
172
|
+
async function fetchAssayNames(rows, headers) {
|
|
113
173
|
const map = new Map();
|
|
114
|
-
const
|
|
115
|
-
|
|
174
|
+
const aeids = [...new Set(rows.map((r) => r.aeid).filter((aeid) => aeid != null))];
|
|
175
|
+
if (aeids.length === 0)
|
|
176
|
+
return map;
|
|
177
|
+
const url = `${EPA_CCTE_BASE}/bioactivity/assay/search/by-aeid/`;
|
|
178
|
+
const res = await apiFetchValidated(url, z.array(ToxcastAssayAnnotationSchema), {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
181
|
+
body: JSON.stringify(aeids.map((aeid) => String(aeid))),
|
|
182
|
+
});
|
|
116
183
|
if (res.isOk() && Array.isArray(res.value)) {
|
|
117
|
-
for (const
|
|
118
|
-
if (
|
|
119
|
-
map.set(
|
|
184
|
+
for (const assay of res.value) {
|
|
185
|
+
if (assay.aeid != null && assay.assayComponentEndpointName) {
|
|
186
|
+
map.set(assay.aeid, assay.assayComponentEndpointName);
|
|
120
187
|
}
|
|
121
188
|
}
|
|
122
189
|
}
|
|
@@ -126,13 +193,12 @@ async function fetchAssayNames(dtxsid, headers) {
|
|
|
126
193
|
// Each schema below both validates one raw CTX hazard row and normalizes it
|
|
127
194
|
// into the curated output shape via `.transform`; `z.infer` is that output
|
|
128
195
|
// type. Every wire field is optional (the API omits absent values); the two
|
|
129
|
-
// numeric fields the API sends as string-or-number
|
|
130
|
-
|
|
131
|
-
const ToxValSchema = z
|
|
196
|
+
// numeric fields the API sends as string-or-number ride `zWireNumber`.
|
|
197
|
+
export const ToxValSchema = z
|
|
132
198
|
.object({
|
|
133
199
|
source: z.string().nullable().optional(),
|
|
134
200
|
toxvalType: z.string().nullable().optional(),
|
|
135
|
-
toxvalNumeric:
|
|
201
|
+
toxvalNumeric: zWireNumber.nullable().optional(),
|
|
136
202
|
toxvalUnits: z.string().nullable().optional(),
|
|
137
203
|
studyType: z.string().nullable().optional(),
|
|
138
204
|
studyDurationClass: z.string().nullable().optional(),
|
|
@@ -141,13 +207,13 @@ const ToxValSchema = z
|
|
|
141
207
|
toxicologicalEffect: z.string().nullable().optional(),
|
|
142
208
|
riskAssessmentClass: z.string().nullable().optional(),
|
|
143
209
|
humanEco: z.string().nullable().optional(),
|
|
144
|
-
year:
|
|
210
|
+
year: zWireNumber.nullable().optional(),
|
|
145
211
|
quality: z.string().nullable().optional(),
|
|
146
212
|
})
|
|
147
213
|
.transform((r) => ({
|
|
148
214
|
source: r.source ?? "",
|
|
149
215
|
toxvalType: r.toxvalType ?? "",
|
|
150
|
-
toxvalNumeric:
|
|
216
|
+
toxvalNumeric: r.toxvalNumeric ?? null,
|
|
151
217
|
toxvalUnits: r.toxvalUnits ?? "",
|
|
152
218
|
studyType: r.studyType ?? "",
|
|
153
219
|
studyDurationClass: r.studyDurationClass ?? "",
|
|
@@ -156,38 +222,46 @@ const ToxValSchema = z
|
|
|
156
222
|
toxicologicalEffect: r.toxicologicalEffect ?? "",
|
|
157
223
|
riskAssessmentClass: r.riskAssessmentClass ?? "",
|
|
158
224
|
humanEco: r.humanEco ?? "",
|
|
159
|
-
year:
|
|
225
|
+
year: r.year ?? null,
|
|
160
226
|
quality: r.quality ?? "",
|
|
161
227
|
}));
|
|
162
|
-
|
|
228
|
+
// The genotoxicity SUMMARY record. It is a roll-up over the genotoxicity
|
|
229
|
+
// reports of a chemical: the two headline assay calls, the overall call, and
|
|
230
|
+
// the report counts behind them. The per-report fields (the assay category, the
|
|
231
|
+
// metabolic activation, the species) belong to the `genetox/details` endpoint,
|
|
232
|
+
// which this client does not call.
|
|
233
|
+
export const GenetoxSchema = z
|
|
163
234
|
.object({
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
235
|
+
ames: z.string().nullable().optional(),
|
|
236
|
+
genetoxCall: z.string().nullable().optional(),
|
|
237
|
+
micronucleus: z.string().nullable().optional(),
|
|
238
|
+
reportsPositive: z.number().nullable().optional(),
|
|
239
|
+
reportsNegative: z.number().nullable().optional(),
|
|
240
|
+
reportsOther: z.number().nullable().optional(),
|
|
241
|
+
clowderDocId: z.string().nullable().optional(),
|
|
171
242
|
})
|
|
172
243
|
.transform((r) => ({
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
244
|
+
ames: r.ames ?? "",
|
|
245
|
+
genetoxCall: r.genetoxCall ?? "",
|
|
246
|
+
micronucleus: r.micronucleus ?? "",
|
|
247
|
+
reportsPositive: r.reportsPositive ?? null,
|
|
248
|
+
reportsNegative: r.reportsNegative ?? null,
|
|
249
|
+
reportsOther: r.reportsOther ?? null,
|
|
250
|
+
clowderDocId: r.clowderDocId ?? "",
|
|
180
251
|
}));
|
|
181
|
-
|
|
252
|
+
// The cancer SUMMARY record. `cancerCall` is the weight-of-evidence annotation,
|
|
253
|
+
// and `exposureRoute` is the route that the annotation applies to.
|
|
254
|
+
export const CancerSchema = z
|
|
182
255
|
.object({
|
|
183
256
|
source: z.string().nullable().optional(),
|
|
184
|
-
|
|
185
|
-
|
|
257
|
+
exposureRoute: z.string().nullable().optional(),
|
|
258
|
+
cancerCall: z.string().nullable().optional(),
|
|
186
259
|
url: z.string().nullable().optional(),
|
|
187
260
|
})
|
|
188
261
|
.transform((r) => ({
|
|
189
262
|
source: r.source ?? "",
|
|
190
|
-
|
|
263
|
+
exposureRoute: r.exposureRoute ?? "",
|
|
264
|
+
cancerCall: r.cancerCall ?? "",
|
|
191
265
|
url: r.url ?? "",
|
|
192
266
|
}));
|
|
193
267
|
/**
|
|
@@ -223,12 +297,6 @@ async function fetchToxval(dtxsid, headers, limit) {
|
|
|
223
297
|
return [];
|
|
224
298
|
return res.value.slice(0, limit);
|
|
225
299
|
}
|
|
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
300
|
async function fetchGenetox(dtxsid, headers, limit) {
|
|
233
301
|
const url = `${EPA_CCTE_BASE}/hazard/genetox/summary/search/by-dtxsid/${dtxsid}`;
|
|
234
302
|
const res = await apiFetchValidated(url, z.array(GenetoxSchema), { headers });
|
|
@@ -246,7 +314,7 @@ async function fetchCancer(dtxsid, headers) {
|
|
|
246
314
|
// Raw CTX chemical-detail wire shape, validated at the fetch boundary. Every
|
|
247
315
|
// field is optional (the API omits absent values); the mapper below normalizes
|
|
248
316
|
// it into `ChemicalDetail`, folding in the resolved `dtxsid` fallback.
|
|
249
|
-
const RawChemicalDetailSchema = z.object({
|
|
317
|
+
export const RawChemicalDetailSchema = z.object({
|
|
250
318
|
dtxsid: z.string().optional(),
|
|
251
319
|
dtxcid: z.string().nullable().optional(),
|
|
252
320
|
casrn: z.string().nullable().optional(),
|
|
@@ -256,7 +324,6 @@ const RawChemicalDetailSchema = z.object({
|
|
|
256
324
|
smiles: z.string().nullable().optional(),
|
|
257
325
|
inchikey: z.string().nullable().optional(),
|
|
258
326
|
monoisotopicMass: z.number().nullable().optional(),
|
|
259
|
-
averageMass: z.number().nullable().optional(),
|
|
260
327
|
qcLevel: z.number().nullable().optional(),
|
|
261
328
|
totalAssays: z.number().nullable().optional(),
|
|
262
329
|
activeAssays: z.number().nullable().optional(),
|
|
@@ -294,7 +361,6 @@ export async function fetchCtxChemicalDetail(dtxsid, headers) {
|
|
|
294
361
|
smiles: d.smiles ?? "",
|
|
295
362
|
inchikey: d.inchikey ?? "",
|
|
296
363
|
monoisotopicMass: d.monoisotopicMass ?? null,
|
|
297
|
-
averageMass: d.averageMass ?? null,
|
|
298
364
|
qcLevel: d.qcLevel ?? null,
|
|
299
365
|
totalAssays: d.totalAssays ?? null,
|
|
300
366
|
activeAssays: d.activeAssays ?? null,
|
|
@@ -324,20 +390,20 @@ export async function fetchCtxChemicalProperties(dtxsid, headers) {
|
|
|
324
390
|
}
|
|
325
391
|
return undefined;
|
|
326
392
|
}
|
|
327
|
-
// SEEM exposure-prediction wire shape
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
393
|
+
// SEEM exposure-prediction wire shape. The endpoint answers with an array.
|
|
394
|
+
// `probabilityPesticde` is the wire name, and it is the own misspelling of the
|
|
395
|
+
// API. The corrected spelling is served nowhere, thus only the wire name is
|
|
396
|
+
// modeled and the mapped output carries the corrected one.
|
|
397
|
+
export const RawSeemPredictionSchema = z.object({
|
|
331
398
|
dtxsid: z.string().optional(),
|
|
332
399
|
productionVolume: z.number().nullable().optional(),
|
|
333
400
|
units: z.string().nullable().optional(),
|
|
334
401
|
probabilityDietary: z.number().nullable().optional(),
|
|
335
402
|
probabilityResidential: z.number().nullable().optional(),
|
|
336
403
|
probabilityPesticde: z.number().nullable().optional(),
|
|
337
|
-
probabilityPesticide: z.number().nullable().optional(),
|
|
338
404
|
probabilityIndustrial: z.number().nullable().optional(),
|
|
339
405
|
});
|
|
340
|
-
const RawHttkRowSchema = z.object({
|
|
406
|
+
export const RawHttkRowSchema = z.object({
|
|
341
407
|
parameter: z.string().nullable().optional(),
|
|
342
408
|
measured: z.number().nullable().optional(),
|
|
343
409
|
predicted: z.number().nullable().optional(),
|
|
@@ -346,7 +412,7 @@ const RawHttkRowSchema = z.object({
|
|
|
346
412
|
species: z.string().nullable().optional(),
|
|
347
413
|
reference: z.string().nullable().optional(),
|
|
348
414
|
});
|
|
349
|
-
const RawFunctionalUseRowSchema = z.object({
|
|
415
|
+
export const RawFunctionalUseRowSchema = z.object({
|
|
350
416
|
functioncategory: z.string().nullable().optional(),
|
|
351
417
|
reportedfunction: z.string().nullable().optional(),
|
|
352
418
|
doctitle: z.string().nullable().optional(),
|
|
@@ -392,10 +458,10 @@ export async function fetchCtxExposureData(dtxsid, headers, opts) {
|
|
|
392
458
|
}
|
|
393
459
|
async function fetchSeem(dtxsid, headers) {
|
|
394
460
|
const url = `${EPA_CCTE_BASE}/exposure/seem/general/search/by-dtxsid/${dtxsid}`;
|
|
395
|
-
const res = await apiFetchValidated(url, z.
|
|
396
|
-
if (res.isErr()
|
|
461
|
+
const res = await apiFetchValidated(url, z.array(RawSeemPredictionSchema), { headers });
|
|
462
|
+
if (res.isErr())
|
|
397
463
|
return undefined;
|
|
398
|
-
const d =
|
|
464
|
+
const d = res.value[0];
|
|
399
465
|
if (!d)
|
|
400
466
|
return undefined;
|
|
401
467
|
return {
|
|
@@ -404,7 +470,7 @@ async function fetchSeem(dtxsid, headers) {
|
|
|
404
470
|
units: d.units ?? "",
|
|
405
471
|
probabilityDietary: d.probabilityDietary ?? null,
|
|
406
472
|
probabilityResidential: d.probabilityResidential ?? null,
|
|
407
|
-
probabilityPesticide: d.probabilityPesticde ??
|
|
473
|
+
probabilityPesticide: d.probabilityPesticde ?? null,
|
|
408
474
|
probabilityIndustrial: d.probabilityIndustrial ?? null,
|
|
409
475
|
};
|
|
410
476
|
}
|
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
* DGIdb aggregates 30+ sources (ChEMBL, DrugBank, TTD, PharmGKB,
|
|
6
6
|
* GuideToPharmacology, CIViC, …) and reports per-interaction source counts,
|
|
7
7
|
* which the caller uses as a confidence signal.
|
|
8
|
+
*
|
|
9
|
+
* Absence policy: the nullability of the GraphQL SDL encodes an absent value.
|
|
10
|
+
* DGIdb answers each requested field with a key and an explicit `null`, thus
|
|
11
|
+
* the SDL gives each modifier and `.optional()` is wrong.
|
|
8
12
|
*/
|
|
13
|
+
import { z } from "zod";
|
|
9
14
|
export interface DgidbInteraction {
|
|
10
15
|
geneName: string;
|
|
11
16
|
drugName: string;
|
|
@@ -31,6 +36,74 @@ export interface DgidbResult {
|
|
|
31
36
|
/** Interactions this input had before `limit` trimmed them. */
|
|
32
37
|
totalInteractions: number;
|
|
33
38
|
}
|
|
39
|
+
/** The response envelope. It is exported so that the golden-fixture table drives it. */
|
|
40
|
+
export declare const DgidbResponseSchema: z.ZodObject<{
|
|
41
|
+
data: z.ZodOptional<z.ZodObject<{
|
|
42
|
+
genes: z.ZodOptional<z.ZodObject<{
|
|
43
|
+
nodes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodNullable<z.ZodObject<{
|
|
44
|
+
name: z.ZodNullable<z.ZodString>;
|
|
45
|
+
conceptId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
46
|
+
interactions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
47
|
+
interactionScore: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
48
|
+
interactionTypes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
49
|
+
type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
50
|
+
directionality: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
51
|
+
}, z.core.$strip>>>;
|
|
52
|
+
drug: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
53
|
+
name: z.ZodOptional<z.ZodString>;
|
|
54
|
+
conceptId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
55
|
+
}, z.core.$strip>>>;
|
|
56
|
+
gene: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
57
|
+
name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
58
|
+
}, z.core.$strip>>>;
|
|
59
|
+
interactionAttributes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
60
|
+
name: z.ZodOptional<z.ZodString>;
|
|
61
|
+
value: z.ZodOptional<z.ZodString>;
|
|
62
|
+
}, z.core.$strip>>>;
|
|
63
|
+
publications: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
64
|
+
pmid: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
65
|
+
}, z.core.$strip>>>;
|
|
66
|
+
sources: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
67
|
+
sourceDbName: z.ZodOptional<z.ZodString>;
|
|
68
|
+
}, z.core.$strip>>>;
|
|
69
|
+
}, z.core.$strip>>>;
|
|
70
|
+
}, z.core.$strip>>>>>;
|
|
71
|
+
}, z.core.$strip>>;
|
|
72
|
+
drugs: z.ZodOptional<z.ZodObject<{
|
|
73
|
+
nodes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodNullable<z.ZodObject<{
|
|
74
|
+
name: z.ZodNullable<z.ZodString>;
|
|
75
|
+
conceptId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
76
|
+
interactions: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
77
|
+
interactionScore: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
78
|
+
interactionTypes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
79
|
+
type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
80
|
+
directionality: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
81
|
+
}, z.core.$strip>>>;
|
|
82
|
+
drug: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
83
|
+
name: z.ZodOptional<z.ZodString>;
|
|
84
|
+
conceptId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
85
|
+
}, z.core.$strip>>>;
|
|
86
|
+
gene: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
87
|
+
name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
88
|
+
}, z.core.$strip>>>;
|
|
89
|
+
interactionAttributes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
90
|
+
name: z.ZodOptional<z.ZodString>;
|
|
91
|
+
value: z.ZodOptional<z.ZodString>;
|
|
92
|
+
}, z.core.$strip>>>;
|
|
93
|
+
publications: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
94
|
+
pmid: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
95
|
+
}, z.core.$strip>>>;
|
|
96
|
+
sources: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
97
|
+
sourceDbName: z.ZodOptional<z.ZodString>;
|
|
98
|
+
}, z.core.$strip>>>;
|
|
99
|
+
}, z.core.$strip>>>;
|
|
100
|
+
}, z.core.$strip>>>>>;
|
|
101
|
+
}, z.core.$strip>>;
|
|
102
|
+
}, z.core.$strip>>;
|
|
103
|
+
errors: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
104
|
+
message: z.ZodOptional<z.ZodString>;
|
|
105
|
+
}, z.core.$strip>>>;
|
|
106
|
+
}, z.core.$strip>;
|
|
34
107
|
export interface DgidbFilters {
|
|
35
108
|
interactionTypes?: string[];
|
|
36
109
|
sourceDbs?: string[];
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* DGIdb aggregates 30+ sources (ChEMBL, DrugBank, TTD, PharmGKB,
|
|
6
6
|
* GuideToPharmacology, CIViC, …) and reports per-interaction source counts,
|
|
7
7
|
* which the caller uses as a confidence signal.
|
|
8
|
+
*
|
|
9
|
+
* Absence policy: the nullability of the GraphQL SDL encodes an absent value.
|
|
10
|
+
* DGIdb answers each requested field with a key and an explicit `null`, thus
|
|
11
|
+
* the SDL gives each modifier and `.optional()` is wrong.
|
|
8
12
|
*/
|
|
9
13
|
import { z } from "zod";
|
|
10
14
|
import { apiFetchValidated, describeApiError } from "./api-utils.js";
|
|
@@ -41,30 +45,31 @@ const DRUG_INTERACTIONS_QUERY = `query($drugs: [String!]!) {
|
|
|
41
45
|
}
|
|
42
46
|
}`;
|
|
43
47
|
// Shape of the DGIdb GraphQL payload as it arrives on the wire. Validated at
|
|
44
|
-
// the fetch boundary; `mapInteraction` normalizes each record.
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// `name.
|
|
48
|
-
// `
|
|
48
|
+
// the fetch boundary; `mapInteraction` normalizes each record. GraphQL always
|
|
49
|
+
// answers a requested field with a key, thus absence is an explicit `null` and
|
|
50
|
+
// never an omission. As a result each modifier below comes from the SDL:
|
|
51
|
+
// `Gene.name`, `InteractionClaimType.type` and `directionality` are nullable,
|
|
52
|
+
// and `GeneConnection.nodes` is a nullable list of nullable elements.
|
|
49
53
|
const RawInteractionSchema = z.object({
|
|
50
54
|
interactionScore: z.number().nullable().optional(),
|
|
51
|
-
interactionTypes: z.array(z.object({ type: z.string().optional(), directionality: z.string().nullable().optional() })).optional(),
|
|
55
|
+
interactionTypes: z.array(z.object({ type: z.string().nullable().optional(), directionality: z.string().nullable().optional() })).optional(),
|
|
52
56
|
drug: z.object({ name: z.string().optional(), conceptId: z.string().nullable().optional() }).nullable().optional(),
|
|
53
|
-
gene: z.object({ name: z.string().optional() }).nullable().optional(),
|
|
57
|
+
gene: z.object({ name: z.string().nullable().optional() }).nullable().optional(),
|
|
54
58
|
interactionAttributes: z.array(z.object({ name: z.string().optional(), value: z.string().optional() })).optional(),
|
|
55
59
|
publications: z.array(z.object({ pmid: z.union([z.string(), z.number()]).optional() })).optional(),
|
|
56
60
|
sources: z.array(z.object({ sourceDbName: z.string().optional() })).optional(),
|
|
57
61
|
});
|
|
58
62
|
const RawNodeSchema = z.object({
|
|
59
|
-
name: z.string(),
|
|
63
|
+
name: z.string().nullable(),
|
|
60
64
|
conceptId: z.string().nullable().optional(),
|
|
61
65
|
interactions: z.array(RawInteractionSchema).optional(),
|
|
62
66
|
});
|
|
63
|
-
|
|
67
|
+
/** The response envelope. It is exported so that the golden-fixture table drives it. */
|
|
68
|
+
export const DgidbResponseSchema = z.object({
|
|
64
69
|
data: z
|
|
65
70
|
.object({
|
|
66
|
-
genes: z.object({ nodes: z.array(RawNodeSchema).
|
|
67
|
-
drugs: z.object({ nodes: z.array(RawNodeSchema).
|
|
71
|
+
genes: z.object({ nodes: z.array(RawNodeSchema.nullable()).nullish() }).optional(),
|
|
72
|
+
drugs: z.object({ nodes: z.array(RawNodeSchema.nullable()).nullish() }).optional(),
|
|
68
73
|
})
|
|
69
74
|
.optional(),
|
|
70
75
|
errors: z.array(z.object({ message: z.string().optional() })).optional(),
|
|
@@ -146,9 +151,16 @@ export async function searchDgidb(inputs, searchType, filters = {}, onPartialErr
|
|
|
146
151
|
const nodes = (isGene ? body.data.genes?.nodes : body.data.drugs?.nodes) ?? [];
|
|
147
152
|
const nodeMap = new Map();
|
|
148
153
|
for (const node of nodes) {
|
|
149
|
-
|
|
154
|
+
// The node list and `Gene.name` are nullable in the SDL. The map keys on
|
|
155
|
+
// the name, thus a null element and a null-named node have no key, and
|
|
156
|
+
// the input that asked for them reports `found: false`.
|
|
157
|
+
const name = node?.name;
|
|
158
|
+
if (!node || !name)
|
|
159
|
+
continue;
|
|
160
|
+
const named = { ...node, name };
|
|
161
|
+
nodeMap.set(name.toLowerCase(), named);
|
|
150
162
|
if (!isGene && node.conceptId)
|
|
151
|
-
nodeMap.set(node.conceptId.toLowerCase(),
|
|
163
|
+
nodeMap.set(node.conceptId.toLowerCase(), named);
|
|
152
164
|
}
|
|
153
165
|
const limit = filters.limit ?? 20;
|
|
154
166
|
return inputs.map((input) => {
|