@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.
Files changed (123) hide show
  1. package/dist/contracts/target-dossier.d.ts +61 -61
  2. package/dist/execution/run-synthesis.js +7 -2
  3. package/dist/lib/zod-issues.d.ts +8 -0
  4. package/dist/lib/zod-issues.js +13 -0
  5. package/dist/literature/sources/semantic-scholar.d.ts +34 -0
  6. package/dist/literature/sources/semantic-scholar.js +15 -4
  7. package/dist/sandbox/cpu-files.d.ts +37 -0
  8. package/dist/sandbox/cpu-files.js +55 -0
  9. package/dist/sandbox/docker-client.d.ts +15 -0
  10. package/dist/sandbox/docker-client.js +100 -11
  11. package/dist/sandbox/k8s-client.d.ts +5 -0
  12. package/dist/sandbox/k8s-client.js +128 -19
  13. package/dist/sandbox/thread-env.d.ts +29 -0
  14. package/dist/sandbox/thread-env.js +49 -0
  15. package/dist/state/data-profile.d.ts +35 -0
  16. package/dist/state/data-profile.js +56 -0
  17. package/dist/state/index.d.ts +1 -1
  18. package/dist/state/index.js +1 -1
  19. package/dist/tasks/data-profile.d.ts +16 -1
  20. package/dist/tasks/data-profile.js +31 -10
  21. package/dist/tools/bio/chembl.js +4 -3
  22. package/dist/tools/bio/drug-gene-interactions.d.ts +2 -14
  23. package/dist/tools/bio/drug-gene-interactions.js +10 -18
  24. package/dist/tools/bio/gene-disease-evidence.d.ts +3 -1
  25. package/dist/tools/bio/gene-disease-evidence.js +12 -9
  26. package/dist/tools/bio/keys.js +1 -0
  27. package/dist/tools/bio/lookup-annotation.d.ts +43 -0
  28. package/dist/tools/bio/lookup-annotation.js +19 -4
  29. package/dist/tools/bio/pubchem.js +5 -3
  30. package/dist/tools/bio/search-clinical-trials.js +5 -4
  31. package/dist/tools/bio/search-geo-datasets.d.ts +39 -9
  32. package/dist/tools/bio/search-geo-datasets.js +38 -20
  33. package/dist/tools/lib/api-utils.d.ts +27 -2
  34. package/dist/tools/lib/api-utils.js +43 -3
  35. package/dist/tools/lib/bgee-client.d.ts +16 -0
  36. package/dist/tools/lib/bgee-client.js +16 -5
  37. package/dist/tools/lib/cbioportal-client.d.ts +20 -0
  38. package/dist/tools/lib/cbioportal-client.js +16 -8
  39. package/dist/tools/lib/chembl-client.d.ts +105 -3
  40. package/dist/tools/lib/chembl-client.js +196 -84
  41. package/dist/tools/lib/clinical-benchmarks-client.d.ts +5 -0
  42. package/dist/tools/lib/clinical-benchmarks-client.js +5 -0
  43. package/dist/tools/lib/clinical-trials-client.d.ts +131 -2
  44. package/dist/tools/lib/clinical-trials-client.js +32 -6
  45. package/dist/tools/lib/clinvar-client.d.ts +35 -0
  46. package/dist/tools/lib/clinvar-client.js +34 -28
  47. package/dist/tools/lib/ctx-ops.d.ts +106 -35
  48. package/dist/tools/lib/ctx-ops.js +135 -69
  49. package/dist/tools/lib/dgidb-client.d.ts +73 -0
  50. package/dist/tools/lib/dgidb-client.js +25 -13
  51. package/dist/tools/lib/disgenet-client.d.ts +117 -24
  52. package/dist/tools/lib/disgenet-client.js +127 -34
  53. package/dist/tools/lib/disgenet-config.d.ts +1 -1
  54. package/dist/tools/lib/disgenet-config.js +5 -3
  55. package/dist/tools/lib/drugbank-client.d.ts +57 -58
  56. package/dist/tools/lib/drugbank-client.js +69 -64
  57. package/dist/tools/lib/drugbank-config.d.ts +6 -1
  58. package/dist/tools/lib/drugbank-config.js +6 -1
  59. package/dist/tools/lib/ema-client.d.ts +53 -0
  60. package/dist/tools/lib/ema-client.js +19 -7
  61. package/dist/tools/lib/ensembl-client.d.ts +4 -0
  62. package/dist/tools/lib/ensembl-client.js +4 -0
  63. package/dist/tools/lib/gwas-catalog-client.d.ts +64 -1
  64. package/dist/tools/lib/gwas-catalog-client.js +21 -6
  65. package/dist/tools/lib/identifier-resolver.d.ts +17 -1
  66. package/dist/tools/lib/identifier-resolver.js +13 -4
  67. package/dist/tools/lib/impc-client.d.ts +4 -0
  68. package/dist/tools/lib/impc-client.js +4 -0
  69. package/dist/tools/lib/iuphar-client.d.ts +10 -5
  70. package/dist/tools/lib/iuphar-client.js +49 -13
  71. package/dist/tools/lib/monarch-client.d.ts +4 -0
  72. package/dist/tools/lib/monarch-client.js +4 -0
  73. package/dist/tools/lib/openfda-client.d.ts +34 -0
  74. package/dist/tools/lib/openfda-client.js +22 -8
  75. package/dist/tools/lib/opentargets-client.d.ts +99 -4
  76. package/dist/tools/lib/opentargets-client.js +97 -37
  77. package/dist/tools/lib/pathway-client.d.ts +45 -0
  78. package/dist/tools/lib/pathway-client.js +88 -8
  79. package/dist/tools/lib/pharmgkb-client.d.ts +35 -1
  80. package/dist/tools/lib/pharmgkb-client.js +22 -6
  81. package/dist/tools/lib/pharmgkb-config.d.ts +8 -1
  82. package/dist/tools/lib/pharmgkb-config.js +8 -1
  83. package/dist/tools/lib/pubchem-client.d.ts +22 -0
  84. package/dist/tools/lib/pubchem-client.js +36 -33
  85. package/dist/tools/lib/pubchem-ops.d.ts +85 -5
  86. package/dist/tools/lib/pubchem-ops.js +96 -68
  87. package/dist/tools/lib/pubmed-client.d.ts +4 -0
  88. package/dist/tools/lib/pubmed-client.js +4 -0
  89. package/dist/tools/lib/string-client.d.ts +52 -0
  90. package/dist/tools/lib/string-client.js +12 -7
  91. package/dist/tools/lib/uniprot-client.d.ts +4 -0
  92. package/dist/tools/lib/uniprot-client.js +4 -0
  93. package/dist/tools/research/context7-docs.d.ts +23 -0
  94. package/dist/tools/research/context7-docs.js +49 -9
  95. package/dist/tools/research/search-github-repos.d.ts +4 -0
  96. package/dist/tools/research/search-github-repos.js +4 -0
  97. package/dist/workflows/execute-target-assessment.js +4 -4
  98. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +2 -1
  99. package/dist/workflows/target-assessment/assemblers/orchestrator.js +3 -3
  100. package/dist/workflows/target-assessment/collectors/index.d.ts +8 -2
  101. package/dist/workflows/target-assessment/collectors/index.js +37 -10
  102. package/dist/workflows/target-assessment/fanout/index.d.ts +2 -1
  103. package/dist/workflows/target-assessment/fanout/index.js +2 -2
  104. package/dist/workflows/target-assessment/lib/safety-corroboration.js +2 -2
  105. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +3 -2
  106. package/dist/workflows/target-assessment/lib/target-identity-filter.js +16 -5
  107. package/dist/workflows/target-assessment/phase4-assemble.d.ts +61 -60
  108. package/dist/workflows/target-assessment/phase4-assemble.js +2 -2
  109. package/dist/workflows/target-assessment/schemas.d.ts +2 -8
  110. package/dist/workflows/target-assessment/schemas.js +8 -2
  111. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +1 -4
  112. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -4
  113. package/package.json +1 -1
  114. package/dist/agents/sandbox/__fixtures__/deps.d.ts +0 -23
  115. package/dist/agents/sandbox/__fixtures__/deps.js +0 -102
  116. package/dist/citations/__fixtures__/resolver.d.ts +0 -3
  117. package/dist/citations/__fixtures__/resolver.js +0 -9
  118. package/dist/loop/__fixtures__/scripted-provider.d.ts +0 -31
  119. package/dist/loop/__fixtures__/scripted-provider.js +0 -81
  120. package/dist/providers/__fixtures__/session.d.ts +0 -17
  121. package/dist/providers/__fixtures__/session.js +0 -20
  122. package/dist/tools/__fixtures__/tool-context.d.ts +0 -13
  123. package/dist/tools/__fixtures__/tool-context.js +0 -22
@@ -2,14 +2,19 @@
2
2
  * Pure async client functions for biological pathway lookups via KEGG and Reactome.
3
3
  *
4
4
  * Used by §3.7 (Pathway Context).
5
+ *
6
+ * Absence policy: Reactome omits the key of an absent value, thus a maybe-absent
7
+ * field carries `.optional()`. KEGG serves plain text, not JSON, thus it has no
8
+ * key to omit.
5
9
  */
6
10
  import { z } from "zod";
7
11
  import { apiFetch, apiFetchValidated, parseTSV } from "./api-utils.js";
8
12
  const KEGG_BASE = "https://rest.kegg.jp";
9
- // The ContentService of the primary host answers 404 on each path. The release
10
- // host serves the same API, and a probe of `search/query`, `data/query/{stId}`
11
- // and `data/participants/{stId}` returned 200 on it. The web pages that a result
12
- // links to stay on the primary host, thus only the API base moves.
13
+ // Both `reactome.org/ContentService` and `release.reactome.org/ContentService`
14
+ // answer 200 on each path that this file calls. The two hosts serve different
15
+ // data releases, thus the same stable identifier gives a different record on
16
+ // each. The release host is the pinned one, and the web pages that a result
17
+ // links to stay on the primary host.
13
18
  const REACTOME_BASE = "https://release.reactome.org/ContentService";
14
19
  export function stripHtmlAndCollapseWs(s) {
15
20
  // Strip tags to a fixpoint: a single pass can leave a `<…>` behind when
@@ -39,11 +44,11 @@ const ReactomeSearchEntrySchema = z.object({
39
44
  stId: z.string().optional(),
40
45
  name: z.string().optional(),
41
46
  });
42
- const ReactomeSearchResponseSchema = z.object({
47
+ export const ReactomeSearchResponseSchema = z.object({
43
48
  results: z.array(z.object({ entries: z.array(ReactomeSearchEntrySchema).optional() })).optional(),
44
49
  });
45
50
  /** Participant list — `searchReactome` reads `displayName` per participant. */
46
- const ReactomeParticipantsSchema = z.array(z.object({ displayName: z.string().optional() }));
51
+ export const ReactomeParticipantsSchema = z.array(z.object({ displayName: z.string().optional() }));
47
52
  /** One Reactome event record, as `/data/query/{stId}` serves it. */
48
53
  const ReactomeEntrySchema = z.object({
49
54
  stId: z.string().optional(),
@@ -51,7 +56,7 @@ const ReactomeEntrySchema = z.object({
51
56
  summation: z.array(z.object({ text: z.string().optional() })).optional(),
52
57
  });
53
58
  /** Participant list — `getPathwayMemberships` reads `refEntities` per participant. */
54
- const ReactomeParticipantEntitiesSchema = z.array(z.object({
59
+ export const ReactomeParticipantEntitiesSchema = z.array(z.object({
55
60
  refEntities: z.array(z.object({ schemaClass: z.string().optional(), identifier: z.string().optional() })).optional(),
56
61
  }));
57
62
  export async function searchKegg(query, organism, maxResults, includeGenes) {
@@ -79,6 +84,81 @@ export async function searchKegg(query, organism, maxResults, includeGenes) {
79
84
  }
80
85
  return pathways;
81
86
  }
87
+ /**
88
+ * Read the KEGG gene identifier of a symbol, for example `hsa:7157` for `TP53`.
89
+ *
90
+ * `find/{organism}/{symbol}` searches the gene names of one organism, and it
91
+ * ranks a partial name match above an exact one. A query for `TP53` puts
92
+ * `TP53BP2` first. Thus the caller matches the symbol against the alias list of
93
+ * each row, and it never takes the first row.
94
+ */
95
+ export function selectKeggGeneId(rows, symbol) {
96
+ const wanted = symbol.trim().toUpperCase();
97
+ for (const cols of rows) {
98
+ const id = (cols[0] ?? "").trim();
99
+ // Column two is `ALIAS1, ALIAS2; definition`.
100
+ const aliases = (cols[1] ?? "").split(";")[0] ?? "";
101
+ for (const alias of aliases.split(",")) {
102
+ if (alias.trim().toUpperCase() === wanted)
103
+ return id;
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+ /** The pathway identifiers of a `link/pathway/{geneId}` answer, without the `path:` prefix. */
109
+ export function parseKeggPathwayLinks(rows) {
110
+ const ids = [];
111
+ for (const cols of rows) {
112
+ const id = (cols[1] ?? "").replace("path:", "").trim();
113
+ if (id)
114
+ ids.push(id);
115
+ }
116
+ return ids;
117
+ }
118
+ /**
119
+ * The pathways that hold one gene, from KEGG.
120
+ *
121
+ * `find/pathway/{query}` matches a pathway NAME only, thus a gene symbol gives
122
+ * an empty body there. The membership path is a gene resolution, then
123
+ * `link/pathway/{geneId}`, then one `list/pathway/{organism}` call for the
124
+ * names. The one list call costs about 22 kB and names every pathway of the
125
+ * organism, thus it replaces one `get/{id}` call for each membership.
126
+ */
127
+ export async function keggPathwaysForGene(geneSymbol, organism, maxResults) {
128
+ const geneRes = await apiFetch(`${KEGG_BASE}/find/${organism}/${encodeURIComponent(geneSymbol)}`, { parseAs: "text" });
129
+ if (geneRes.isErr())
130
+ return [];
131
+ const geneId = selectKeggGeneId(parseTSV(geneRes.value), geneSymbol);
132
+ if (!geneId)
133
+ return [];
134
+ const linkRes = await apiFetch(`${KEGG_BASE}/link/pathway/${encodeURIComponent(geneId)}`, { parseAs: "text" });
135
+ if (linkRes.isErr())
136
+ return [];
137
+ const pathwayIds = parseKeggPathwayLinks(parseTSV(linkRes.value)).slice(0, maxResults);
138
+ if (pathwayIds.length === 0)
139
+ return [];
140
+ const names = await keggPathwayNames(organism);
141
+ return pathwayIds.map((id) => ({
142
+ id,
143
+ name: names.get(id) ?? id,
144
+ source: "kegg",
145
+ url: `https://www.kegg.jp/pathway/${id}`,
146
+ }));
147
+ }
148
+ /** The pathway vocabulary of one organism, keyed by the pathway identifier. */
149
+ async function keggPathwayNames(organism) {
150
+ const names = new Map();
151
+ const res = await apiFetch(`${KEGG_BASE}/list/pathway/${encodeURIComponent(organism)}`, { parseAs: "text" });
152
+ if (res.isErr())
153
+ return names;
154
+ for (const cols of parseTSV(res.value)) {
155
+ const id = (cols[0] ?? "").trim();
156
+ const name = (cols[1] ?? "").trim();
157
+ if (id && name)
158
+ names.set(id, name);
159
+ }
160
+ return names;
161
+ }
82
162
  export async function searchReactome(query, species, maxResults, includeGenes) {
83
163
  const params = new URLSearchParams({
84
164
  query,
@@ -195,7 +275,7 @@ export async function getPathwayById(id) {
195
275
  }
196
276
  /** Pathway memberships for a gene symbol — used directly by §3.7 collector. */
197
277
  export async function getPathwayMemberships(geneSymbol) {
198
- const [kegg, reactome] = await Promise.all([searchKegg(geneSymbol, "hsa", 25, false), searchReactome(geneSymbol, "Homo sapiens", 25, false)]);
278
+ const [kegg, reactome] = await Promise.all([keggPathwaysForGene(geneSymbol, "hsa", 25), searchReactome(geneSymbol, "Homo sapiens", 25, false)]);
199
279
  // For Reactome pathways, fetch participant UniProts (bounded to avoid rate limits).
200
280
  await Promise.all(reactome.slice(0, 50).map(async (pw) => {
201
281
  try {
@@ -1,9 +1,43 @@
1
1
  /**
2
- * Pure async client functions for PharmGKB clinical annotations.
2
+ * Pure async client functions for the PharmGKB clinical annotations, which
3
+ * ClinPGx now serves.
3
4
  *
4
5
  * Public API, no key required. Matching is an exact field filter, not a search:
5
6
  * `location.genes.symbol` for a gene, `relatedChemicals.name` for a drug.
7
+ *
8
+ * Absence policy: the sampled ClinPGx payloads carry no explicit `null`, and a
9
+ * record omits the key of an absent value. Thus a maybe-absent field carries
10
+ * `.optional()`, not `.nullable()`.
6
11
  */
12
+ import { z } from "zod";
13
+ export declare const PharmgkbAnnotationSchema: z.ZodObject<{
14
+ location: z.ZodOptional<z.ZodObject<{
15
+ genes: z.ZodOptional<z.ZodArray<z.ZodObject<{
16
+ symbol: z.ZodOptional<z.ZodString>;
17
+ }, z.core.$strip>>>;
18
+ }, z.core.$strip>>;
19
+ relatedChemicals: z.ZodOptional<z.ZodArray<z.ZodObject<{
20
+ name: z.ZodOptional<z.ZodString>;
21
+ }, z.core.$strip>>>;
22
+ levelOfEvidence: z.ZodOptional<z.ZodObject<{
23
+ term: z.ZodOptional<z.ZodString>;
24
+ }, z.core.$strip>>;
25
+ }, z.core.$strip>;
26
+ export declare const PharmgkbResponseSchema: z.ZodObject<{
27
+ data: z.ZodOptional<z.ZodArray<z.ZodObject<{
28
+ location: z.ZodOptional<z.ZodObject<{
29
+ genes: z.ZodOptional<z.ZodArray<z.ZodObject<{
30
+ symbol: z.ZodOptional<z.ZodString>;
31
+ }, z.core.$strip>>>;
32
+ }, z.core.$strip>>;
33
+ relatedChemicals: z.ZodOptional<z.ZodArray<z.ZodObject<{
34
+ name: z.ZodOptional<z.ZodString>;
35
+ }, z.core.$strip>>>;
36
+ levelOfEvidence: z.ZodOptional<z.ZodObject<{
37
+ term: z.ZodOptional<z.ZodString>;
38
+ }, z.core.$strip>>;
39
+ }, z.core.$strip>>>;
40
+ }, z.core.$strip>;
7
41
  /**
8
42
  * One PharmGKB clinical annotation.
9
43
  *
@@ -1,27 +1,43 @@
1
1
  /**
2
- * Pure async client functions for PharmGKB clinical annotations.
2
+ * Pure async client functions for the PharmGKB clinical annotations, which
3
+ * ClinPGx now serves.
3
4
  *
4
5
  * Public API, no key required. Matching is an exact field filter, not a search:
5
6
  * `location.genes.symbol` for a gene, `relatedChemicals.name` for a drug.
7
+ *
8
+ * Absence policy: the sampled ClinPGx payloads carry no explicit `null`, and a
9
+ * record omits the key of an absent value. Thus a maybe-absent field carries
10
+ * `.optional()`, not `.nullable()`.
6
11
  */
7
12
  import { z } from "zod";
8
13
  import { apiFetchValidated, describeApiError } from "./api-utils.js";
9
14
  import { PHARMGKB_BASE, PHARMGKB_HEADERS } from "./pharmgkb-config.js";
10
- // Raw PharmGKB clinicalAnnotation wire shape, validated at the fetch boundary.
11
- // Every field is optional because the API omits absent values.
12
- const PharmgkbAnnotationSchema = z.object({
15
+ // Raw ClinPGx clinicalAnnotation wire shape, validated at the fetch boundary.
16
+ export const PharmgkbAnnotationSchema = z.object({
13
17
  location: z.object({ genes: z.array(z.object({ symbol: z.string().optional() })).optional() }).optional(),
14
18
  relatedChemicals: z.array(z.object({ name: z.string().optional() })).optional(),
15
19
  levelOfEvidence: z.object({ term: z.string().optional() }).optional(),
16
20
  });
17
- const PharmgkbResponseSchema = z.object({
21
+ // `data` is polymorphic by status: an array of annotation records on HTTP 200,
22
+ // and an error OBJECT (`{"errors":[…]}`) on a 4xx. The undeclared sibling key
23
+ // `status` carries `"success"` or `"fail"`. This schema models the 200 arm only,
24
+ // because the 4xx arm never reaches it — `apiFetch` reports a non-ok status as
25
+ // `http_status` before the body is parsed.
26
+ export const PharmgkbResponseSchema = z.object({
18
27
  data: z.array(PharmgkbAnnotationSchema).optional(),
19
28
  });
20
29
  export async function searchPharmgkb(query, searchType, limit = 20) {
21
30
  const filter = searchType === "gene" ? `location.genes.symbol=${encodeURIComponent(query)}` : `relatedChemicals.name=${encodeURIComponent(query)}`;
22
31
  const res = await apiFetchValidated(`${PHARMGKB_BASE}/clinicalAnnotation?${filter}`, PharmgkbResponseSchema, { headers: PHARMGKB_HEADERS });
23
- if (res.isErr())
32
+ if (res.isErr()) {
33
+ // ClinPGx answers "no results matching criteria" with HTTP 404, not with
34
+ // an empty array. A gene or a drug that the corpus does not annotate is
35
+ // an expected miss, thus it gives an empty result. Every other failure
36
+ // still throws, because it is an outage or a contract break.
37
+ if (res.error.type === "http_status" && res.error.status === 404)
38
+ return { annotations: [], totalFound: 0 };
24
39
  throw new Error(describeApiError(res.error));
40
+ }
25
41
  const data = res.value?.data ?? [];
26
42
  const annotations = data.slice(0, limit).map((ann) => ({
27
43
  gene: ann.location?.genes?.map((g) => g.symbol).join(", ") ?? query,
@@ -1,4 +1,11 @@
1
- export declare const PHARMGKB_BASE = "https://api.pharmgkb.org/v1/data";
1
+ /**
2
+ * Shared constants for the PharmGKB clinical-annotation API.
3
+ *
4
+ * PharmGKB is now ClinPGx, and `api.pharmgkb.org` no longer resolves. The
5
+ * successor host serves the same paths, the same envelope, and the same field
6
+ * names, thus only the host changes here.
7
+ */
8
+ export declare const PHARMGKB_BASE = "https://api.clinpgx.org/v1/data";
2
9
  export declare const PHARMGKB_HEADERS: {
3
10
  readonly Accept: "application/json";
4
11
  };
@@ -1,2 +1,9 @@
1
- export const PHARMGKB_BASE = "https://api.pharmgkb.org/v1/data";
1
+ /**
2
+ * Shared constants for the PharmGKB clinical-annotation API.
3
+ *
4
+ * PharmGKB is now ClinPGx, and `api.pharmgkb.org` no longer resolves. The
5
+ * successor host serves the same paths, the same envelope, and the same field
6
+ * names, thus only the host changes here.
7
+ */
8
+ export const PHARMGKB_BASE = "https://api.clinpgx.org/v1/data";
2
9
  export const PHARMGKB_HEADERS = { Accept: "application/json" };
@@ -7,7 +7,12 @@
7
7
  * exposes molecular weight + SMILES + InChI properties that let us
8
8
  * heuristically classify peptides, small molecules, and proteins without a
9
9
  * hand-curated override table.
10
+ *
11
+ * Absence policy: PubChem omits the key of an absent value, per `minOccurs=0`
12
+ * in its XSD, and it never sends an explicit `null`. Thus a maybe-absent field
13
+ * carries `.optional()`, not `.nullable()`.
10
14
  */
15
+ import { z } from "zod";
11
16
  export interface PubChemCompoundProperties {
12
17
  cid: number;
13
18
  molecularWeight: number | null;
@@ -16,6 +21,23 @@ export interface PubChemCompoundProperties {
16
21
  hBondDonorCount: number | null;
17
22
  hBondAcceptorCount: number | null;
18
23
  }
24
+ export declare const PubChemPropertyResponseSchema: z.ZodObject<{
25
+ PropertyTable: z.ZodOptional<z.ZodObject<{
26
+ Properties: z.ZodOptional<z.ZodArray<z.ZodObject<{
27
+ CID: z.ZodOptional<z.ZodNumber>;
28
+ MolecularWeight: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<number | null, string | number>>>;
29
+ InChIKey: z.ZodOptional<z.ZodString>;
30
+ ConnectivitySMILES: z.ZodOptional<z.ZodString>;
31
+ HBondDonorCount: z.ZodOptional<z.ZodNumber>;
32
+ HBondAcceptorCount: z.ZodOptional<z.ZodNumber>;
33
+ }, z.core.$strip>>>;
34
+ }, z.core.$strip>>;
35
+ }, z.core.$strip>;
36
+ export declare const PubChemCidListResponseSchema: z.ZodObject<{
37
+ IdentifierList: z.ZodOptional<z.ZodObject<{
38
+ CID: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
39
+ }, z.core.$strip>>;
40
+ }, z.core.$strip>;
19
41
  export declare function getCompoundPropertiesByCID(cid: number): Promise<PubChemCompoundProperties | null>;
20
42
  /**
21
43
  * Look up a PubChem compound by InChI key (the canonical hash of the 3-D
@@ -7,61 +7,63 @@
7
7
  * exposes molecular weight + SMILES + InChI properties that let us
8
8
  * heuristically classify peptides, small molecules, and proteins without a
9
9
  * hand-curated override table.
10
+ *
11
+ * Absence policy: PubChem omits the key of an absent value, per `minOccurs=0`
12
+ * in its XSD, and it never sends an explicit `null`. Thus a maybe-absent field
13
+ * carries `.optional()`, not `.nullable()`.
10
14
  */
11
15
  import { z } from "zod";
12
- import { apiFetchValidated, describeApiError } from "./api-utils.js";
16
+ import { apiFetchValidated, describeApiError, zWireNumber } from "./api-utils.js";
13
17
  import { PUBCHEM_BASE, PUBCHEM_HEADERS } from "./pubchem-config.js";
14
18
  const PROPERTY_FIELDS = [
15
19
  "MolecularWeight",
16
20
  "InChIKey",
17
- // The PUG-REST schema renamed the small-molecule SMILES field to
18
- // ConnectivitySMILES in late 2024; CanonicalSMILES still works but is
19
- // deprecated. We request both and fall back through them.
21
+ // PubChem retired the response key `CanonicalSMILES`, and it answers a request
22
+ // for that name with `ConnectivitySMILES`. Thus the request names the live key.
20
23
  "ConnectivitySMILES",
21
- "CanonicalSMILES",
22
24
  "HBondDonorCount",
23
25
  "HBondAcceptorCount",
24
26
  ].join(",");
25
27
  // A single record from the PUG-REST `PropertyTable.Properties` array, validated
26
- // at the fetch boundary. Every field is optional — PubChem omits absent
27
- // properties, and a row missing `CID` is handled gracefully by the caller's
28
- // guard (returns null) rather than being a contract break.
28
+ // at the fetch boundary. PubChem omits an absent property and it never sends
29
+ // `null`, thus each property carries `.optional()` and a declared type. Under
30
+ // zod 4 a bare `z.unknown()` is required, which rejects an omission.
29
31
  const RawPubChemPropertySchema = z.object({
30
32
  CID: z.number().optional(),
31
- MolecularWeight: z.unknown(),
32
- InChIKey: z.unknown(),
33
- ConnectivitySMILES: z.unknown(),
34
- CanonicalSMILES: z.unknown(),
35
- HBondDonorCount: z.unknown(),
36
- HBondAcceptorCount: z.unknown(),
33
+ // PUG-REST serializes MolecularWeight as a string (e.g. "180.16") to
34
+ // preserve significant figures, thus it reads through the wire-number helper.
35
+ MolecularWeight: zWireNumber.optional(),
36
+ InChIKey: z.string().optional(),
37
+ ConnectivitySMILES: z.string().optional(),
38
+ HBondDonorCount: z.number().optional(),
39
+ HBondAcceptorCount: z.number().optional(),
37
40
  });
38
- const PubChemPropertyResponseSchema = z.object({
41
+ export const PubChemPropertyResponseSchema = z.object({
39
42
  PropertyTable: z.object({ Properties: z.array(RawPubChemPropertySchema).optional() }).optional(),
40
43
  });
41
- const PubChemCidListResponseSchema = z.object({
44
+ export const PubChemCidListResponseSchema = z.object({
42
45
  IdentifierList: z.object({ CID: z.array(z.number()).optional() }).optional(),
43
46
  });
44
- function parseNumber(value) {
45
- if (value == null)
46
- return null;
47
- if (typeof value === "number")
48
- return Number.isFinite(value) ? value : null;
49
- if (typeof value === "string") {
50
- const n = parseFloat(value);
51
- return Number.isFinite(n) ? n : null;
52
- }
53
- return null;
54
- }
55
47
  function mapProps(raw) {
56
48
  return {
57
49
  cid: raw.CID ?? 0,
58
- molecularWeight: parseNumber(raw.MolecularWeight),
59
- inchiKey: typeof raw.InChIKey === "string" ? raw.InChIKey : null,
60
- canonicalSmiles: typeof raw.ConnectivitySMILES === "string" ? raw.ConnectivitySMILES : typeof raw.CanonicalSMILES === "string" ? raw.CanonicalSMILES : null,
61
- hBondDonorCount: typeof raw.HBondDonorCount === "number" ? raw.HBondDonorCount : parseNumber(raw.HBondDonorCount),
62
- hBondAcceptorCount: typeof raw.HBondAcceptorCount === "number" ? raw.HBondAcceptorCount : parseNumber(raw.HBondAcceptorCount),
50
+ molecularWeight: raw.MolecularWeight ?? null,
51
+ inchiKey: raw.InChIKey ?? null,
52
+ canonicalSmiles: raw.ConnectivitySMILES ?? null,
53
+ hBondDonorCount: raw.HBondDonorCount ?? null,
54
+ hBondAcceptorCount: raw.HBondAcceptorCount ?? null,
63
55
  };
64
56
  }
57
+ /**
58
+ * Does the record hold a property?
59
+ *
60
+ * PubChem answers HTTP 200 for a CID that does not exist, with one row that
61
+ * carries the `CID` alone. Thus a record with no property is an absence, and a
62
+ * caller must not read it as a compound.
63
+ */
64
+ function carriesProperties(props) {
65
+ return Object.entries(props).some(([key, value]) => key !== "cid" && value !== null);
66
+ }
65
67
  export async function getCompoundPropertiesByCID(cid) {
66
68
  const url = `${PUBCHEM_BASE}/compound/cid/${cid}/property/${PROPERTY_FIELDS}/JSON`;
67
69
  const res = await apiFetchValidated(url, PubChemPropertyResponseSchema, {
@@ -75,7 +77,8 @@ export async function getCompoundPropertiesByCID(cid) {
75
77
  const props = res.value.PropertyTable?.Properties?.[0];
76
78
  if (!props || typeof props.CID !== "number")
77
79
  return null;
78
- return mapProps(props);
80
+ const mapped = mapProps(props);
81
+ return carriesProperties(mapped) ? mapped : null;
79
82
  }
80
83
  /**
81
84
  * Look up a PubChem compound by InChI key (the canonical hash of the 3-D
@@ -4,14 +4,18 @@
4
4
  * One home for the wire schemas, the URL construction, and the "PubChem 404
5
5
  * means no data" contract, so a caller cannot drift from it: every function
6
6
  * returns an empty collection for a 404 and throws on any other API failure.
7
+ *
8
+ * Absence policy: PubChem omits the key of an absent value, per `minOccurs=0`
9
+ * in its XSD, and it never sends an explicit `null`. Thus a maybe-absent field
10
+ * carries `.optional()`, not `.nullable()`.
7
11
  */
8
12
  import { z } from "zod";
9
13
  export type PubChemSearchBy = "name" | "smiles" | "inchi" | "inchikey" | "cid";
10
14
  declare const PugPropertyRowSchema: z.ZodPipe<z.ZodObject<{
11
15
  CID: z.ZodOptional<z.ZodNumber>;
12
- CanonicalSMILES: z.ZodOptional<z.ZodString>;
16
+ ConnectivitySMILES: z.ZodOptional<z.ZodString>;
13
17
  MolecularFormula: z.ZodOptional<z.ZodString>;
14
- MolecularWeight: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
18
+ MolecularWeight: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<number | null, string | number>>>;
15
19
  IUPACName: z.ZodOptional<z.ZodString>;
16
20
  InChI: z.ZodOptional<z.ZodString>;
17
21
  InChIKey: z.ZodOptional<z.ZodString>;
@@ -37,9 +41,9 @@ declare const PugPropertyRowSchema: z.ZodPipe<z.ZodObject<{
37
41
  complexity: number | null;
38
42
  }, {
39
43
  CID?: number | undefined;
40
- CanonicalSMILES?: string | undefined;
44
+ ConnectivitySMILES?: string | undefined;
41
45
  MolecularFormula?: string | undefined;
42
- MolecularWeight?: string | number | undefined;
46
+ MolecularWeight?: number | null | undefined;
43
47
  IUPACName?: string | undefined;
44
48
  InChI?: string | undefined;
45
49
  InChIKey?: string | undefined;
@@ -51,10 +55,68 @@ declare const PugPropertyRowSchema: z.ZodPipe<z.ZodObject<{
51
55
  Complexity?: number | undefined;
52
56
  }>>;
53
57
  export type PubChemCompound = z.infer<typeof PugPropertyRowSchema>;
58
+ export declare const PubChemPropertyResponseSchema: z.ZodObject<{
59
+ PropertyTable: z.ZodOptional<z.ZodObject<{
60
+ Properties: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodObject<{
61
+ CID: z.ZodOptional<z.ZodNumber>;
62
+ ConnectivitySMILES: z.ZodOptional<z.ZodString>;
63
+ MolecularFormula: z.ZodOptional<z.ZodString>;
64
+ MolecularWeight: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<number | null, string | number>>>;
65
+ IUPACName: z.ZodOptional<z.ZodString>;
66
+ InChI: z.ZodOptional<z.ZodString>;
67
+ InChIKey: z.ZodOptional<z.ZodString>;
68
+ XLogP: z.ZodOptional<z.ZodNumber>;
69
+ TPSA: z.ZodOptional<z.ZodNumber>;
70
+ HBondDonorCount: z.ZodOptional<z.ZodNumber>;
71
+ HBondAcceptorCount: z.ZodOptional<z.ZodNumber>;
72
+ RotatableBondCount: z.ZodOptional<z.ZodNumber>;
73
+ Complexity: z.ZodOptional<z.ZodNumber>;
74
+ }, z.core.$strip>, z.ZodTransform<{
75
+ cid: number;
76
+ canonicalSmiles: string | null;
77
+ molecularFormula: string | null;
78
+ molecularWeight: number | null;
79
+ iupacName: string | null;
80
+ inchi: string | null;
81
+ inchiKey: string | null;
82
+ xlogp: number | null;
83
+ tpsa: number | null;
84
+ hbondDonorCount: number | null;
85
+ hbondAcceptorCount: number | null;
86
+ rotatableBondCount: number | null;
87
+ complexity: number | null;
88
+ }, {
89
+ CID?: number | undefined;
90
+ ConnectivitySMILES?: string | undefined;
91
+ MolecularFormula?: string | undefined;
92
+ MolecularWeight?: number | null | undefined;
93
+ IUPACName?: string | undefined;
94
+ InChI?: string | undefined;
95
+ InChIKey?: string | undefined;
96
+ XLogP?: number | undefined;
97
+ TPSA?: number | undefined;
98
+ HBondDonorCount?: number | undefined;
99
+ HBondAcceptorCount?: number | undefined;
100
+ RotatableBondCount?: number | undefined;
101
+ Complexity?: number | undefined;
102
+ }>>>>;
103
+ }, z.core.$strip>>;
104
+ }, z.core.$strip>;
54
105
  export interface PubChemCrossRef {
55
- source: string;
106
+ /** The registry that the identifier pattern names, or `null` when no pattern matches. */
107
+ source: string | null;
56
108
  id: string;
57
109
  }
110
+ export declare const PubChemXrefResponseSchema: z.ZodObject<{
111
+ InformationList: z.ZodOptional<z.ZodObject<{
112
+ Information: z.ZodOptional<z.ZodArray<z.ZodObject<{
113
+ CID: z.ZodOptional<z.ZodNumber>;
114
+ RegistryID: z.ZodOptional<z.ZodArray<z.ZodString>>;
115
+ }, z.core.$strip>>>;
116
+ }, z.core.$strip>>;
117
+ }, z.core.$strip>;
118
+ /** The registry of one identifier, or `null` when no pattern matches it. */
119
+ export declare function classifyRegistryId(id: string): string | null;
58
120
  export interface PubChemAssay {
59
121
  aid: number;
60
122
  assayName: string | null;
@@ -62,6 +124,17 @@ export interface PubChemAssay {
62
124
  activityOutcome: string | null;
63
125
  activityValue: number | null;
64
126
  }
127
+ export declare const PugAssaySummarySchema: z.ZodObject<{
128
+ Table: z.ZodOptional<z.ZodObject<{
129
+ Columns: z.ZodOptional<z.ZodObject<{
130
+ Column: z.ZodOptional<z.ZodArray<z.ZodString>>;
131
+ }, z.core.$strip>>;
132
+ Row: z.ZodOptional<z.ZodArray<z.ZodObject<{
133
+ Cell: z.ZodOptional<z.ZodArray<z.ZodString>>;
134
+ }, z.core.$strip>>>;
135
+ }, z.core.$strip>>;
136
+ }, z.core.$strip>;
137
+ type PugAssaySummary = z.infer<typeof PugAssaySummarySchema>;
65
138
  /** Resolve a compound to its identity + computed properties. Empty array = no match. */
66
139
  export declare function fetchPubchemCompounds(query: string, searchBy: PubChemSearchBy): Promise<PubChemCompound[]>;
67
140
  /** External registry ids for a CID. Empty array = the CID is in no external registry. */
@@ -71,4 +144,11 @@ export declare function fetchPubchemAssays(cid: number, opts: {
71
144
  activeOnly: boolean;
72
145
  limit: number;
73
146
  }): Promise<PubChemAssay[]>;
147
+ /**
148
+ * Read the assay table by its wire headings.
149
+ *
150
+ * The `targetName` of a record is the target accession, because the table names
151
+ * no protein. The accession resolves against UniProt or against a ChEMBL target.
152
+ */
153
+ export declare function parseAssaySummary(data: PugAssaySummary): PubChemAssay[];
74
154
  export {};