@inflexa-ai/harness 0.30.3 → 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 (109) 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/tools/bio/chembl.js +4 -3
  8. package/dist/tools/bio/drug-gene-interactions.d.ts +2 -14
  9. package/dist/tools/bio/drug-gene-interactions.js +10 -18
  10. package/dist/tools/bio/gene-disease-evidence.d.ts +3 -1
  11. package/dist/tools/bio/gene-disease-evidence.js +12 -9
  12. package/dist/tools/bio/keys.js +1 -0
  13. package/dist/tools/bio/lookup-annotation.d.ts +43 -0
  14. package/dist/tools/bio/lookup-annotation.js +19 -4
  15. package/dist/tools/bio/pubchem.js +5 -3
  16. package/dist/tools/bio/search-clinical-trials.js +5 -4
  17. package/dist/tools/bio/search-geo-datasets.d.ts +39 -9
  18. package/dist/tools/bio/search-geo-datasets.js +38 -20
  19. package/dist/tools/lib/api-utils.d.ts +27 -2
  20. package/dist/tools/lib/api-utils.js +43 -3
  21. package/dist/tools/lib/bgee-client.d.ts +16 -0
  22. package/dist/tools/lib/bgee-client.js +16 -5
  23. package/dist/tools/lib/cbioportal-client.d.ts +20 -0
  24. package/dist/tools/lib/cbioportal-client.js +16 -8
  25. package/dist/tools/lib/chembl-client.d.ts +105 -3
  26. package/dist/tools/lib/chembl-client.js +196 -84
  27. package/dist/tools/lib/clinical-benchmarks-client.d.ts +5 -0
  28. package/dist/tools/lib/clinical-benchmarks-client.js +5 -0
  29. package/dist/tools/lib/clinical-trials-client.d.ts +131 -2
  30. package/dist/tools/lib/clinical-trials-client.js +32 -6
  31. package/dist/tools/lib/clinvar-client.d.ts +35 -0
  32. package/dist/tools/lib/clinvar-client.js +34 -28
  33. package/dist/tools/lib/ctx-ops.d.ts +106 -35
  34. package/dist/tools/lib/ctx-ops.js +135 -69
  35. package/dist/tools/lib/dgidb-client.d.ts +73 -0
  36. package/dist/tools/lib/dgidb-client.js +25 -13
  37. package/dist/tools/lib/disgenet-client.d.ts +117 -24
  38. package/dist/tools/lib/disgenet-client.js +127 -34
  39. package/dist/tools/lib/disgenet-config.d.ts +1 -1
  40. package/dist/tools/lib/disgenet-config.js +5 -3
  41. package/dist/tools/lib/drugbank-client.d.ts +57 -58
  42. package/dist/tools/lib/drugbank-client.js +69 -64
  43. package/dist/tools/lib/drugbank-config.d.ts +6 -1
  44. package/dist/tools/lib/drugbank-config.js +6 -1
  45. package/dist/tools/lib/ema-client.d.ts +53 -0
  46. package/dist/tools/lib/ema-client.js +19 -7
  47. package/dist/tools/lib/ensembl-client.d.ts +4 -0
  48. package/dist/tools/lib/ensembl-client.js +4 -0
  49. package/dist/tools/lib/gwas-catalog-client.d.ts +64 -1
  50. package/dist/tools/lib/gwas-catalog-client.js +21 -6
  51. package/dist/tools/lib/identifier-resolver.d.ts +17 -1
  52. package/dist/tools/lib/identifier-resolver.js +13 -4
  53. package/dist/tools/lib/impc-client.d.ts +4 -0
  54. package/dist/tools/lib/impc-client.js +4 -0
  55. package/dist/tools/lib/iuphar-client.d.ts +10 -5
  56. package/dist/tools/lib/iuphar-client.js +49 -13
  57. package/dist/tools/lib/monarch-client.d.ts +4 -0
  58. package/dist/tools/lib/monarch-client.js +4 -0
  59. package/dist/tools/lib/openfda-client.d.ts +34 -0
  60. package/dist/tools/lib/openfda-client.js +22 -8
  61. package/dist/tools/lib/opentargets-client.d.ts +99 -4
  62. package/dist/tools/lib/opentargets-client.js +97 -37
  63. package/dist/tools/lib/pathway-client.d.ts +45 -0
  64. package/dist/tools/lib/pathway-client.js +88 -8
  65. package/dist/tools/lib/pharmgkb-client.d.ts +35 -1
  66. package/dist/tools/lib/pharmgkb-client.js +22 -6
  67. package/dist/tools/lib/pharmgkb-config.d.ts +8 -1
  68. package/dist/tools/lib/pharmgkb-config.js +8 -1
  69. package/dist/tools/lib/pubchem-client.d.ts +22 -0
  70. package/dist/tools/lib/pubchem-client.js +36 -33
  71. package/dist/tools/lib/pubchem-ops.d.ts +85 -5
  72. package/dist/tools/lib/pubchem-ops.js +96 -68
  73. package/dist/tools/lib/pubmed-client.d.ts +4 -0
  74. package/dist/tools/lib/pubmed-client.js +4 -0
  75. package/dist/tools/lib/string-client.d.ts +52 -0
  76. package/dist/tools/lib/string-client.js +12 -7
  77. package/dist/tools/lib/uniprot-client.d.ts +4 -0
  78. package/dist/tools/lib/uniprot-client.js +4 -0
  79. package/dist/tools/research/context7-docs.d.ts +23 -0
  80. package/dist/tools/research/context7-docs.js +49 -9
  81. package/dist/tools/research/search-github-repos.d.ts +4 -0
  82. package/dist/tools/research/search-github-repos.js +4 -0
  83. package/dist/workflows/execute-target-assessment.js +4 -4
  84. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +2 -1
  85. package/dist/workflows/target-assessment/assemblers/orchestrator.js +3 -3
  86. package/dist/workflows/target-assessment/collectors/index.d.ts +8 -2
  87. package/dist/workflows/target-assessment/collectors/index.js +37 -10
  88. package/dist/workflows/target-assessment/fanout/index.d.ts +2 -1
  89. package/dist/workflows/target-assessment/fanout/index.js +2 -2
  90. package/dist/workflows/target-assessment/lib/safety-corroboration.js +2 -2
  91. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +3 -2
  92. package/dist/workflows/target-assessment/lib/target-identity-filter.js +16 -5
  93. package/dist/workflows/target-assessment/phase4-assemble.d.ts +61 -60
  94. package/dist/workflows/target-assessment/phase4-assemble.js +2 -2
  95. package/dist/workflows/target-assessment/schemas.d.ts +2 -8
  96. package/dist/workflows/target-assessment/schemas.js +8 -2
  97. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +1 -4
  98. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -4
  99. package/package.json +1 -1
  100. package/dist/agents/sandbox/__fixtures__/deps.d.ts +0 -23
  101. package/dist/agents/sandbox/__fixtures__/deps.js +0 -102
  102. package/dist/citations/__fixtures__/resolver.d.ts +0 -3
  103. package/dist/citations/__fixtures__/resolver.js +0 -9
  104. package/dist/loop/__fixtures__/scripted-provider.d.ts +0 -31
  105. package/dist/loop/__fixtures__/scripted-provider.js +0 -81
  106. package/dist/providers/__fixtures__/session.d.ts +0 -17
  107. package/dist/providers/__fixtures__/session.js +0 -20
  108. package/dist/tools/__fixtures__/tool-context.d.ts +0 -13
  109. package/dist/tools/__fixtures__/tool-context.js +0 -22
@@ -4,14 +4,20 @@
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
- import { apiFetchValidated, describeApiError } from "./api-utils.js";
13
+ import { apiFetchValidated, describeApiError, parseWireNumber, zWireNumber } from "./api-utils.js";
10
14
  import { PUBCHEM_HEADERS as HEADERS, PUBCHEM_BASE } from "./pubchem-config.js";
11
15
  const PROPERTY_LIST = [
12
16
  "MolecularFormula",
13
17
  "MolecularWeight",
14
- "CanonicalSMILES",
18
+ // PubChem retired the response key `CanonicalSMILES`, and it answers a request
19
+ // for that name with `ConnectivitySMILES`. Thus the request names the live key.
20
+ "ConnectivitySMILES",
15
21
  "InChI",
16
22
  "InChIKey",
17
23
  "IUPACName",
@@ -31,12 +37,11 @@ const PROPERTY_LIST = [
31
37
  const PugPropertyRowSchema = z
32
38
  .object({
33
39
  CID: z.number().optional(),
34
- CanonicalSMILES: z.string().optional(),
40
+ ConnectivitySMILES: z.string().optional(),
35
41
  MolecularFormula: z.string().optional(),
36
42
  // PUG-REST serializes MolecularWeight as a string (e.g. "180.16") to
37
- // preserve significant figures; accept both and let the transform's
38
- // `Number(...)` normalize it.
39
- MolecularWeight: z.union([z.number(), z.string()]).optional(),
43
+ // preserve significant figures, thus it reads through the wire-number helper.
44
+ MolecularWeight: zWireNumber.optional(),
40
45
  IUPACName: z.string().optional(),
41
46
  InChI: z.string().optional(),
42
47
  InChIKey: z.string().optional(),
@@ -49,9 +54,9 @@ const PugPropertyRowSchema = z
49
54
  })
50
55
  .transform((raw) => ({
51
56
  cid: raw.CID ?? 0,
52
- canonicalSmiles: raw.CanonicalSMILES ?? null,
57
+ canonicalSmiles: raw.ConnectivitySMILES ?? null,
53
58
  molecularFormula: raw.MolecularFormula ?? null,
54
- molecularWeight: raw.MolecularWeight != null ? Number(raw.MolecularWeight) : null,
59
+ molecularWeight: raw.MolecularWeight ?? null,
55
60
  iupacName: raw.IUPACName ?? null,
56
61
  inchi: raw.InChI ?? null,
57
62
  inchiKey: raw.InChIKey ?? null,
@@ -62,50 +67,77 @@ const PugPropertyRowSchema = z
62
67
  rotatableBondCount: raw.RotatableBondCount ?? null,
63
68
  complexity: raw.Complexity ?? null,
64
69
  }));
65
- const PubChemPropertyResponseSchema = z.object({
70
+ export const PubChemPropertyResponseSchema = z.object({
66
71
  PropertyTable: z.object({ Properties: z.array(PugPropertyRowSchema).optional() }).optional(),
67
72
  });
68
73
  // The PUG-REST xrefs wire shape, validated at the fetch boundary: one
69
- // `Information` entry per CID with parallel `RegistryID[]` / `SourceName[]`
70
- // arrays. Every field is optional — PubChem omits absent values.
71
- const PubChemXrefResponseSchema = z.object({
74
+ // `Information` entry per CID with its `RegistryID[]` list. Every field is
75
+ // optional — PubChem omits absent values.
76
+ export const PubChemXrefResponseSchema = z.object({
72
77
  InformationList: z
73
78
  .object({
74
79
  Information: z
75
80
  .array(z.object({
76
81
  CID: z.number().optional(),
77
82
  RegistryID: z.array(z.string()).optional(),
78
- SourceName: z.array(z.string()).optional(),
79
83
  }))
80
84
  .optional(),
81
85
  })
82
86
  .optional(),
83
87
  });
84
- // The PUG-REST assay-summary wire shape (a table of columns + rows), validated
85
- // at the fetch boundary; `parseAssaySummary` maps the heading-indexed cells into
86
- // `PubChemAssay` records. Every field is optional — PubChem omits absent values.
87
- const PugAssaySummarySchema = z.object({
88
+ /**
89
+ * The registry of each identifier pattern that PubChem serves in `RegistryID`.
90
+ *
91
+ * The `RegistryID` and `SourceName` lists of the endpoint are not parallel and
92
+ * they hold a different number of entries, thus an index cannot pair them. The
93
+ * identifier itself carries the registry, and the pattern reads it.
94
+ *
95
+ * A loose pattern runs last, thus a specific registry claims its own id first.
96
+ */
97
+ const REGISTRY_PATTERNS = [
98
+ ["ChEMBL", /^CHEMBL\d+$/],
99
+ ["ChEBI", /^CHEBI:\d+$/],
100
+ ["DrugBank", /^DB\d{5}$/],
101
+ ["KEGG", /^[CD]\d{5}$/],
102
+ ["HMDB", /^HMDB\d+$/],
103
+ ["CAS", /^\d{2,7}-\d{2}-\d$/],
104
+ // A PDB id is four characters that start with a digit. The lookahead demands a
105
+ // letter as well, thus a plain registry number does not read as a structure.
106
+ ["PDB", /^[1-9](?=.*[A-Za-z])[0-9A-Za-z]{3}$/],
107
+ // A UNII is ten alphanumeric characters, and the lookahead demands a letter
108
+ // among them. A plain ten-digit registry number, such as `0000050782`, matches
109
+ // the pattern without it, and the row then names the wrong registry.
110
+ ["UNII", /^(?=.*[A-Z])[A-Z0-9]{10}$/],
111
+ ];
112
+ /** The registry of one identifier, or `null` when no pattern matches it. */
113
+ export function classifyRegistryId(id) {
114
+ for (const [source, pattern] of REGISTRY_PATTERNS) {
115
+ if (pattern.test(id))
116
+ return source;
117
+ }
118
+ return null;
119
+ }
120
+ // The PUG-REST assay-summary wire shape, validated at the fetch boundary: one
121
+ // `Column` list of plain heading strings, and one `Cell` list of plain strings
122
+ // for each row, with an entry for each column. An empty cell is the empty
123
+ // string. `parseAssaySummary` maps the heading-indexed cells into `PubChemAssay`
124
+ // records. Every field is optional — PubChem omits absent values.
125
+ export const PugAssaySummarySchema = z.object({
88
126
  Table: z
89
127
  .object({
90
- Columns: z
91
- .object({
92
- Column: z.array(z.object({ Heading: z.string().optional() })).optional(),
93
- })
94
- .optional(),
95
- Row: z
96
- .array(z.object({
97
- Cell: z
98
- .array(z.object({
99
- intval: z.number().optional(),
100
- fval: z.number().optional(),
101
- sval: z.string().optional(),
102
- }))
103
- .optional(),
104
- }))
105
- .optional(),
128
+ Columns: z.object({ Column: z.array(z.string()).optional() }).optional(),
129
+ Row: z.array(z.object({ Cell: z.array(z.string()).optional() })).optional(),
106
130
  })
107
131
  .optional(),
108
132
  });
133
+ /** The wire headings that the assay summary carries. They hold spaces and a unit. */
134
+ const ASSAY_HEADINGS = {
135
+ aid: "AID",
136
+ assayName: "Assay Name",
137
+ targetAccession: "Target Accession",
138
+ activityOutcome: "Activity Outcome",
139
+ activityValue: "Activity Value [uM]",
140
+ };
109
141
  /** Build the PUG-REST URL path segment for the given search type. */
110
142
  function buildNamespace(searchBy, query) {
111
143
  switch (searchBy) {
@@ -135,11 +167,21 @@ export async function fetchPubchemCompounds(query, searchBy) {
135
167
  throw new Error(describeApiError(res.error));
136
168
  }
137
169
  // Already validated + normalized by PugPropertyRowSchema's transform.
138
- return res.value.PropertyTable?.Properties ?? [];
170
+ return (res.value.PropertyTable?.Properties ?? []).filter(carriesProperties);
171
+ }
172
+ /**
173
+ * Does the row hold a property?
174
+ *
175
+ * PubChem answers HTTP 200 for a CID that does not exist, with one row that
176
+ * carries the `CID` alone. Thus a row with no property is an absence, and a
177
+ * caller must not read it as a compound.
178
+ */
179
+ function carriesProperties(row) {
180
+ return Object.entries(row).some(([key, value]) => key !== "cid" && value !== null);
139
181
  }
140
182
  /** External registry ids for a CID. Empty array = the CID is in no external registry. */
141
183
  export async function fetchPubchemCrossRefs(cid) {
142
- const url = `${PUBCHEM_BASE}/compound/cid/${cid}/xrefs/RegistryID,SourceName/JSON`;
184
+ const url = `${PUBCHEM_BASE}/compound/cid/${cid}/xrefs/RegistryID/JSON`;
143
185
  const res = await apiFetchValidated(url, PubChemXrefResponseSchema, { headers: HEADERS });
144
186
  if (res.isErr()) {
145
187
  // PubChem returns 404 when the CID has no cross-references — expected.
@@ -148,20 +190,10 @@ export async function fetchPubchemCrossRefs(cid) {
148
190
  throw new Error(describeApiError(res.error));
149
191
  }
150
192
  // PUG-REST xrefs returns an InformationList with one entry per CID.
151
- // Each entry has parallel RegistryID[] and SourceName[] arrays.
152
193
  const info = res.value.InformationList?.Information?.[0];
153
194
  if (!info?.RegistryID?.length)
154
195
  return [];
155
- const crossRefs = [];
156
- const registryIds = info.RegistryID;
157
- const sourceNames = info.SourceName ?? [];
158
- for (let i = 0; i < registryIds.length; i++) {
159
- crossRefs.push({
160
- source: sourceNames[i] ?? "Unknown",
161
- id: registryIds[i] ?? "",
162
- });
163
- }
164
- return crossRefs;
196
+ return info.RegistryID.map((id) => ({ source: classifyRegistryId(id), id }));
165
197
  }
166
198
  /** Bioassay screening summaries for a CID. Empty array = never screened. */
167
199
  export async function fetchPubchemAssays(cid, opts) {
@@ -179,39 +211,35 @@ export async function fetchPubchemAssays(cid, opts) {
179
211
  }
180
212
  return assays.slice(0, opts.limit);
181
213
  }
182
- function parseAssaySummary(data) {
214
+ /**
215
+ * Read the assay table by its wire headings.
216
+ *
217
+ * The `targetName` of a record is the target accession, because the table names
218
+ * no protein. The accession resolves against UniProt or against a ChEMBL target.
219
+ */
220
+ export function parseAssaySummary(data) {
183
221
  const columns = data.Table?.Columns?.Column ?? [];
184
222
  const rows = data.Table?.Row ?? [];
185
- // Find column indices by heading
186
223
  const headingIndex = new Map();
187
- for (let i = 0; i < columns.length; i++) {
188
- if (columns[i].Heading) {
189
- headingIndex.set(columns[i].Heading, i);
190
- }
224
+ for (const [index, heading] of columns.entries()) {
225
+ headingIndex.set(heading, index);
191
226
  }
192
- const aidIdx = headingIndex.get("AID");
193
- const nameIdx = headingIndex.get("AssayName");
194
- const targetIdx = headingIndex.get("TargetName");
195
- const outcomeIdx = headingIndex.get("ActivityOutcome");
196
- const valueIdx = headingIndex.get("ActivityValue");
227
+ const aidIdx = headingIndex.get(ASSAY_HEADINGS.aid);
228
+ const nameIdx = headingIndex.get(ASSAY_HEADINGS.assayName);
229
+ const targetIdx = headingIndex.get(ASSAY_HEADINGS.targetAccession);
230
+ const outcomeIdx = headingIndex.get(ASSAY_HEADINGS.activityOutcome);
231
+ const valueIdx = headingIndex.get(ASSAY_HEADINGS.activityValue);
197
232
  const results = [];
198
233
  for (const row of rows) {
199
234
  const cells = row.Cell ?? [];
235
+ // An empty cell is the empty string, which carries no value.
200
236
  const getCellStr = (idx) => {
201
- if (idx === undefined || idx >= cells.length)
202
- return null;
203
- return cells[idx]?.sval ?? null;
204
- };
205
- const getCellNum = (idx) => {
206
- if (idx === undefined || idx >= cells.length)
237
+ if (idx === undefined)
207
238
  return null;
208
239
  const cell = cells[idx];
209
- if (cell?.fval !== undefined)
210
- return cell.fval;
211
- if (cell?.intval !== undefined)
212
- return cell.intval;
213
- return null;
240
+ return cell !== undefined && cell !== "" ? cell : null;
214
241
  };
242
+ const getCellNum = (idx) => parseWireNumber(getCellStr(idx));
215
243
  results.push({
216
244
  aid: getCellNum(aidIdx) ?? 0,
217
245
  assayName: getCellStr(nameIdx),
@@ -3,6 +3,10 @@
3
3
  *
4
4
  * Used by §3.11 (Key Papers), §3.10.4 (Preclinical literature), and the
5
5
  * pubmed-index Phase-1 collector.
6
+ *
7
+ * Absence policy: NCBI E-utilities omits the key of an absent value, and it
8
+ * never sends an explicit `null`. Thus a maybe-absent field carries
9
+ * `.optional()`, not `.nullable()`.
6
10
  */
7
11
  import { type ArticleDetail, type ArticleSection, type FullTextResult, type PubmedSearchOptions, type PubMedSummary } from "../../literature/sources/pubmed.js";
8
12
  export type { PubMedSummary, ArticleDetail, ArticleSection, FullTextResult };
@@ -3,6 +3,10 @@
3
3
  *
4
4
  * Used by §3.11 (Key Papers), §3.10.4 (Preclinical literature), and the
5
5
  * pubmed-index Phase-1 collector.
6
+ *
7
+ * Absence policy: NCBI E-utilities omits the key of an absent value, and it
8
+ * never sends an explicit `null`. Thus a maybe-absent field carries
9
+ * `.optional()`, not `.nullable()`.
6
10
  */
7
11
  import { createPubmedSource, } from "../../literature/sources/pubmed.js";
8
12
  function pubmedSource(ncbiApiKey) {
@@ -2,7 +2,12 @@
2
2
  * Pure async client functions for STRING DB protein-protein interactions.
3
3
  *
4
4
  * Used by §3.8 (PPI Network).
5
+ *
6
+ * Absence policy: STRING omits the key of an absent value, and it never sends
7
+ * an explicit `null`. Thus a maybe-absent field carries `.optional()`, not
8
+ * `.nullable()`.
5
9
  */
10
+ import { z } from "zod";
6
11
  export interface StringInteraction {
7
12
  proteinA: string;
8
13
  proteinB: string;
@@ -25,6 +30,53 @@ export interface InteractionOptions {
25
30
  minScore?: number;
26
31
  limit?: number;
27
32
  }
33
+ export declare const StringInteractionSchema: z.ZodPipe<z.ZodObject<{
34
+ preferredName_A: z.ZodOptional<z.ZodString>;
35
+ preferredName_B: z.ZodOptional<z.ZodString>;
36
+ score: z.ZodOptional<z.ZodNumber>;
37
+ escore: z.ZodOptional<z.ZodNumber>;
38
+ dscore: z.ZodOptional<z.ZodNumber>;
39
+ tscore: z.ZodOptional<z.ZodNumber>;
40
+ }, z.core.$strip>, z.ZodTransform<{
41
+ proteinA: string;
42
+ proteinB: string;
43
+ score: number;
44
+ experimentalScore: number | undefined;
45
+ databaseScore: number | undefined;
46
+ textminingScore: number | undefined;
47
+ }, {
48
+ preferredName_A?: string | undefined;
49
+ preferredName_B?: string | undefined;
50
+ score?: number | undefined;
51
+ escore?: number | undefined;
52
+ dscore?: number | undefined;
53
+ tscore?: number | undefined;
54
+ }>>;
55
+ export declare const StringEnrichmentSchema: z.ZodPipe<z.ZodObject<{
56
+ category: z.ZodOptional<z.ZodString>;
57
+ term: z.ZodOptional<z.ZodString>;
58
+ description: z.ZodOptional<z.ZodString>;
59
+ p_value: z.ZodOptional<z.ZodNumber>;
60
+ fdr: z.ZodOptional<z.ZodNumber>;
61
+ number_of_genes: z.ZodOptional<z.ZodNumber>;
62
+ preferredNames: z.ZodOptional<z.ZodArray<z.ZodString>>;
63
+ }, z.core.$strip>, z.ZodTransform<{
64
+ category: string;
65
+ term: string;
66
+ description: string;
67
+ pValue: number;
68
+ fdr: number;
69
+ geneCount: number;
70
+ genes: string[];
71
+ }, {
72
+ category?: string | undefined;
73
+ term?: string | undefined;
74
+ description?: string | undefined;
75
+ p_value?: number | undefined;
76
+ fdr?: number | undefined;
77
+ number_of_genes?: number | undefined;
78
+ preferredNames?: string[] | undefined;
79
+ }>>;
28
80
  /** Fetch interaction partners (one-hop) for the given identifiers. */
29
81
  export declare function getInteractionPartners(identifiers: string[], options?: InteractionOptions): Promise<StringInteraction[]>;
30
82
  /**
@@ -2,6 +2,10 @@
2
2
  * Pure async client functions for STRING DB protein-protein interactions.
3
3
  *
4
4
  * Used by §3.8 (PPI Network).
5
+ *
6
+ * Absence policy: STRING omits the key of an absent value, and it never sends
7
+ * an explicit `null`. Thus a maybe-absent field carries `.optional()`, not
8
+ * `.nullable()`.
5
9
  */
6
10
  import { z } from "zod";
7
11
  import { apiFetchValidated, describeApiError } from "./api-utils.js";
@@ -23,7 +27,7 @@ function buildParams(identifiers, species, extra = {}) {
23
27
  // `.transform(...)` half normalizes it into the `StringInteraction` we return. Parsing IS
24
28
  // the validation: `apiFetchValidated` runs this over the JSON, so a field whose type drifts
25
29
  // is rejected as `invalid_response` rather than silently mis-mapped.
26
- const StringInteractionSchema = z
30
+ export const StringInteractionSchema = z
27
31
  .object({
28
32
  preferredName_A: z.string().optional(),
29
33
  preferredName_B: z.string().optional(),
@@ -40,10 +44,11 @@ const StringInteractionSchema = z
40
44
  databaseScore: d.dscore ?? undefined,
41
45
  textminingScore: d.tscore ?? undefined,
42
46
  }));
43
- // One STRING enrichment row. `preferredNames` legitimately arrives as either a string array
44
- // or a single comma-joined string, so the schema accepts the union and the transform folds
45
- // both into a `string[]`.
46
- const StringEnrichmentSchema = z
47
+ // One STRING enrichment row. The `/api/json` renderer sends `preferredNames` as a
48
+ // JSON array of gene names on every row, thus the schema declares an array. A
49
+ // comma-joined string is a form of the tsv renderer, which this client never
50
+ // calls.
51
+ export const StringEnrichmentSchema = z
47
52
  .object({
48
53
  category: z.string().optional(),
49
54
  term: z.string().optional(),
@@ -51,7 +56,7 @@ const StringEnrichmentSchema = z
51
56
  p_value: z.number().optional(),
52
57
  fdr: z.number().optional(),
53
58
  number_of_genes: z.number().optional(),
54
- preferredNames: z.union([z.array(z.string()), z.string()]).optional(),
59
+ preferredNames: z.array(z.string()).optional(),
55
60
  })
56
61
  .transform((d) => ({
57
62
  category: d.category ?? "",
@@ -60,7 +65,7 @@ const StringEnrichmentSchema = z
60
65
  pValue: d.p_value ?? 1,
61
66
  fdr: d.fdr ?? 1,
62
67
  geneCount: d.number_of_genes ?? 0,
63
- genes: Array.isArray(d.preferredNames) ? d.preferredNames : (d.preferredNames ?? "").split(",").filter(Boolean),
68
+ genes: d.preferredNames ?? [],
64
69
  }));
65
70
  /** Fetch interaction partners (one-hop) for the given identifiers. */
66
71
  export async function getInteractionPartners(identifiers, options = {}) {
@@ -8,6 +8,10 @@
8
8
  * - Protein family text — used together with IUPHAR family-target
9
9
  * resolution to identify sibling receptors in the same pharmacological
10
10
  * family (replaces the hand-curated RELATED_FAMILY_UNIPROTS map).
11
+ *
12
+ * Absence policy: the OpenAPI document of UniProt marks no field as required,
13
+ * and the API omits the key of an absent value. Thus a maybe-absent field
14
+ * carries `.optional()`, not `.nullable()`.
11
15
  */
12
16
  export interface UniProtRecord {
13
17
  primaryAccession: string;
@@ -8,6 +8,10 @@
8
8
  * - Protein family text — used together with IUPHAR family-target
9
9
  * resolution to identify sibling receptors in the same pharmacological
10
10
  * family (replaces the hand-curated RELATED_FAMILY_UNIPROTS map).
11
+ *
12
+ * Absence policy: the OpenAPI document of UniProt marks no field as required,
13
+ * and the API omits the key of an absent value. Thus a maybe-absent field
14
+ * carries `.optional()`, not `.nullable()`.
11
15
  */
12
16
  import { z } from "zod";
13
17
  import { apiFetchValidated, describeApiError } from "./api-utils.js";
@@ -5,7 +5,12 @@
5
5
  * documentation and code examples at runtime. Two-step flow:
6
6
  * 1. resolveLibraryId — find the Context7 library ID for a package name
7
7
  * 2. queryDocs — query documentation for a resolved library
8
+ *
9
+ * Absence policy: the OpenAPI document of Context7 marks no field as nullable,
10
+ * and the sampled payloads carry no explicit `null`. Thus an absent value is an
11
+ * omitted key, and a maybe-absent field carries `.optional()`.
8
12
  */
13
+ import { z } from "zod";
9
14
  type ResolveLibraryIdOutput = {
10
15
  found: false;
11
16
  } | {
@@ -20,6 +25,24 @@ type QueryDocsOutput = {
20
25
  found: true;
21
26
  documentation: string;
22
27
  };
28
+ export declare const Context7SearchResponseSchema: z.ZodObject<{
29
+ results: z.ZodOptional<z.ZodArray<z.ZodObject<{
30
+ id: z.ZodString;
31
+ title: z.ZodString;
32
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
33
+ }, z.core.$strip>>>;
34
+ }, z.core.$strip>;
35
+ export declare const Context7DocsResponseSchema: z.ZodObject<{
36
+ snippets: z.ZodArray<z.ZodObject<{
37
+ codeTitle: z.ZodString;
38
+ codeDescription: z.ZodString;
39
+ codeId: z.ZodString;
40
+ codeList: z.ZodArray<z.ZodObject<{
41
+ language: z.ZodString;
42
+ code: z.ZodString;
43
+ }, z.core.$strip>>;
44
+ }, z.core.$strip>>;
45
+ }, z.core.$strip>;
23
46
  export declare const resolveLibraryIdTool: import("../define-tool.js").Tool<{
24
47
  libraryName: string;
25
48
  query: string;
@@ -5,19 +5,42 @@
5
5
  * documentation and code examples at runtime. Two-step flow:
6
6
  * 1. resolveLibraryId — find the Context7 library ID for a package name
7
7
  * 2. queryDocs — query documentation for a resolved library
8
+ *
9
+ * Absence policy: the OpenAPI document of Context7 marks no field as nullable,
10
+ * and the sampled payloads carry no explicit `null`. Thus an absent value is an
11
+ * omitted key, and a maybe-absent field carries `.optional()`.
8
12
  */
9
13
  import { ok } from "neverthrow";
10
14
  import { z } from "zod";
11
15
  import { defineTool } from "../define-tool.js";
12
16
  import { apiFetchValidated, describeApiError } from "../lib/api-utils.js";
13
17
  const CONTEXT7_BASE = "https://context7.com/api/v1";
14
- // Raw Context7 wire shapes, validated at the fetch boundary. `id`/`name` stay
18
+ // Raw Context7 wire shapes, validated at the fetch boundary. `id`/`title` stay
15
19
  // required — they are read without a guard — while genuinely-optional fields
16
20
  // carry `.optional()` so a partial-but-valid response still parses.
17
- const Context7SearchResponseSchema = z.object({
18
- results: z.array(z.object({ id: z.string(), name: z.string(), description: z.string().nullable().optional() })).optional(),
21
+ export const Context7SearchResponseSchema = z.object({
22
+ results: z.array(z.object({ id: z.string(), title: z.string(), description: z.string().nullable().optional() })).optional(),
23
+ });
24
+ // The docs route answers one `snippets` array, and each snippet holds its code in
25
+ // `codeList`. There is no single documentation text on the wire, thus the tool
26
+ // composes one from the snippets below.
27
+ export const Context7DocsResponseSchema = z.object({
28
+ snippets: z.array(z.object({
29
+ codeTitle: z.string(),
30
+ codeDescription: z.string(),
31
+ codeId: z.string(),
32
+ codeList: z.array(z.object({ language: z.string(), code: z.string() })),
33
+ })),
19
34
  });
20
- const Context7DocsResponseSchema = z.object({ content: z.string().optional() });
35
+ /** Render the snippets of one docs answer as a single text, in the layout of the Context7 text route. */
36
+ function renderSnippets(snippets) {
37
+ return snippets
38
+ .map((snippet) => {
39
+ const code = snippet.codeList.map((example) => `\`\`\`${example.language}\n${example.code}\n\`\`\``).join("\n\n");
40
+ return `### ${snippet.codeTitle}\n\nSource: ${snippet.codeId}\n\n${snippet.codeDescription}\n\n${code}`;
41
+ })
42
+ .join("\n\n--------------------------------\n\n");
43
+ }
21
44
  export const resolveLibraryIdTool = defineTool({
22
45
  id: "resolve_library_id",
23
46
  description: "Resolve a package name (e.g., 'scanpy', 'DESeq2', 'scikit-learn') to a Context7 library ID. Call this BEFORE `query_docs` to get the library ID needed for documentation lookup. Returns matching libraries ranked by relevance.",
@@ -37,7 +60,9 @@ export const resolveLibraryIdTool = defineTool({
37
60
  return ok({
38
61
  found: true,
39
62
  libraryId: best.id,
40
- name: best.name,
63
+ // The display name of a library is `title` on the wire. The output keeps
64
+ // the `name` key, because that key is the contract that a caller reads.
65
+ name: best.title,
41
66
  description: best.description ?? "",
42
67
  });
43
68
  },
@@ -46,15 +71,30 @@ export const queryDocsTool = defineTool({
46
71
  id: "query_docs",
47
72
  description: "Query up-to-date documentation and code examples for a library. You must call `resolve_library_id` first to get the libraryId. Use this to verify function signatures, parameters, and usage patterns before writing code.",
48
73
  inputSchema: z.object({
49
- libraryId: z.string().describe("Context7 library ID from `resolve_library_id` (for example '/scverse/scanpy')."),
74
+ libraryId: z.string().describe("Context7 library ID exactly as `resolve_library_id` gave it — an owner and a library, for example '/scverse/scanpy'."),
50
75
  query: z.string().describe("Specific question about the library (e.g., 'rank_genes_groups function parameters and usage')."),
51
76
  }),
52
77
  describeCall: "none",
53
78
  execute: async ({ libraryId, query }) => {
54
- const result = await apiFetchValidated(`${CONTEXT7_BASE}/docs?libraryId=${encodeURIComponent(libraryId)}&query=${encodeURIComponent(query)}`, Context7DocsResponseSchema, { maxRetries: 1 });
55
- if (result.isErr())
79
+ // The library id is the path of the route, not a query parameter. Each
80
+ // segment is encoded on its own, thus the separators of the id survive and
81
+ // a leading or trailing slash makes no difference.
82
+ const path = libraryId
83
+ .split("/")
84
+ .filter((segment) => segment !== "")
85
+ .map((segment) => encodeURIComponent(segment))
86
+ .join("/");
87
+ const result = await apiFetchValidated(`${CONTEXT7_BASE}/${path}?type=json&topic=${encodeURIComponent(query)}`, Context7DocsResponseSchema, {
88
+ maxRetries: 1,
89
+ });
90
+ if (result.isErr()) {
91
+ // Context7 answers 404 for a library that it does not hold. A stale id is
92
+ // an expected outcome, thus it becomes a data variant.
93
+ if (result.error.type === "http_status" && result.error.status === 404)
94
+ return ok({ found: false });
56
95
  throw new Error(`Context7 query failed: ${describeApiError(result.error)}`);
57
- const documentation = result.value.content ?? "";
96
+ }
97
+ const documentation = renderSnippets(result.value.snippets);
58
98
  // No documentation for the query is an expected outcome.
59
99
  if (!documentation)
60
100
  return ok({ found: false });
@@ -4,6 +4,10 @@
4
4
  * (lower rate limit); `GITHUB_TOKEN` raises the limit.
5
5
  *
6
6
  * Same wire call and envelope as the legacy tool. Stateless HTTP; no deps.
7
+ *
8
+ * Absence policy: the OpenAPI document of GitHub marks `description` and
9
+ * `language` as required and nullable, thus an absent value is an explicit
10
+ * `null`. The OpenAPI document gives the modifier of each field.
7
11
  */
8
12
  type SearchGithubReposOutput = {
9
13
  success: false;
@@ -4,6 +4,10 @@
4
4
  * (lower rate limit); `GITHUB_TOKEN` raises the limit.
5
5
  *
6
6
  * Same wire call and envelope as the legacy tool. Stateless HTTP; no deps.
7
+ *
8
+ * Absence policy: the OpenAPI document of GitHub marks `description` and
9
+ * `language` as required and nullable, thus an absent value is an explicit
10
+ * `null`. The OpenAPI document gives the modifier of each field.
7
11
  */
8
12
  import { ok } from "neverthrow";
9
13
  import { z } from "zod";
@@ -149,7 +149,7 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
149
149
  // terminal block dispatches `markFailed({kind: "target-unresolved"})`.
150
150
  await emitProgress(deps.pool, logger, input.assessmentId, "resolving");
151
151
  const resolved = await DBOS.runStep(async () => {
152
- const r = await resolveTarget(input.target);
152
+ const r = await resolveTarget(input.target, logger);
153
153
  return {
154
154
  assessmentId: input.assessmentId,
155
155
  goal: input.goal ?? null,
@@ -166,7 +166,7 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
166
166
  // and `Phase1Bundle` is reconstructed by manifest key below.
167
167
  await emitProgress(deps.pool, logger, input.assessmentId, "collecting");
168
168
  const manifest = COLLECTOR_MANIFEST;
169
- const collectorCtx = { ncbiApiKey: deps.ncbiApiKey };
169
+ const collectorCtx = { ncbiApiKey: deps.ncbiApiKey, logger };
170
170
  const collectorPairs = await Promise.all(manifest.map((entry) => DBOS.runStep(() => entry.run(resolved, collectorCtx), {
171
171
  name: `ta-collector:${entry.id}`,
172
172
  })
@@ -285,7 +285,7 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
285
285
  const [perModulatorFaersResults, perTrialAesResults, perModulatorPolypharmResults, perClassDrugAesResults] = await Promise.all([
286
286
  runFanoutBlock(triageShortlist, "modulator-faers", (item) => item.moleculeChemblId, faersForOneModulator),
287
287
  runFanoutBlock(trialItems, "trial-aes", (item) => item.nctId, aesForOneTrial),
288
- runFanoutBlock(polypharmItems, "modulator-polypharm", (item) => item.moleculeChemblId, polypharmForOneModulator),
288
+ runFanoutBlock(polypharmItems, "modulator-polypharm", (item) => item.moleculeChemblId, (item) => polypharmForOneModulator(item, logger)),
289
289
  runFanoutBlock(classDrugItems, "class-drug-aes", (item) => item.moleculeChemblId, aesForOneClassDrug),
290
290
  ]);
291
291
  const phase3 = {
@@ -307,7 +307,7 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
307
307
  model: deps.decisionModel,
308
308
  ...(deps.usageRecorder ? { usageRecorder: deps.usageRecorder } : {}),
309
309
  };
310
- const phase4 = await DBOS.runStep(() => phase4Assemble(deps.pool, phase3, annotatorDeps), {
310
+ const phase4 = await DBOS.runStep(() => phase4Assemble(deps.pool, phase3, annotatorDeps, logger), {
311
311
  name: "ta-phase4-assemble",
312
312
  });
313
313
  // (§5.8-pre) Approval-precedent grounding — one deterministic openFDA
@@ -21,6 +21,7 @@ import type { Phase2Bundle } from "../steps/phase2-aggregate.js";
21
21
  import type { Phase3Bundle } from "../steps/phase3-aggregate.js";
22
22
  import type { ResolvedTarget } from "../schemas.js";
23
23
  import type { Pool } from "pg";
24
+ import type { Logger } from "../../../lib/logger.js";
24
25
  import type { ClinicalConsequenceAnnotatorDeps } from "../lib/clinical-consequence-annotator.js";
25
26
  import { HIGH_EXPRESSION_TPM_THRESHOLD } from "../lib/expression-constants.js";
26
27
  export { HIGH_EXPRESSION_TPM_THRESHOLD };
@@ -52,7 +53,7 @@ export declare function assembleTractability(resolved: ResolvedTarget, ot: OpenT
52
53
  * attached here — phase5-persist computes it after synthesis is stamped
53
54
  * so the final derived values reflect liability bullets and safety flags.
54
55
  */
55
- export declare function assembleDossier(pool: Pool, phase2: Phase2Bundle, phase3?: Phase3Bundle, annotatorDeps?: ClinicalConsequenceAnnotatorDeps): Promise<DossierBody>;
56
+ export declare function assembleDossier(pool: Pool, phase2: Phase2Bundle, phase3?: Phase3Bundle, annotatorDeps?: ClinicalConsequenceAnnotatorDeps, logger?: Logger): Promise<DossierBody>;
56
57
  type ReconcileFaersClassPrecedent = {
57
58
  coverage?: "available" | "queried_no_data" | "not_loaded";
58
59
  data?: {
@@ -223,7 +223,7 @@ export function assembleTractability(resolved, ot, drugs) {
223
223
  * attached here — phase5-persist computes it after synthesis is stamped
224
224
  * so the final derived values reflect liability bullets and safety flags.
225
225
  */
226
- export async function assembleDossier(pool, phase2, phase3, annotatorDeps) {
226
+ export async function assembleDossier(pool, phase2, phase3, annotatorDeps, logger) {
227
227
  const fanout = phase3?.fanout;
228
228
  const faersAgg = aggregateFaersAcrossModulators(fanout);
229
229
  const trialAesAgg = aggregateTrialAes(fanout);
@@ -235,8 +235,8 @@ export async function assembleDossier(pool, phase2, phase3, annotatorDeps) {
235
235
  const familyComplexesCollector = phase2.phase1.collectors.familyComplexes;
236
236
  const familyComplexesBundle = familyComplexesCollector.coverage === "available" ? familyComplexesCollector.data : null;
237
237
  const [onTargetChemblIds, familySiblingUniprots] = await Promise.all([
238
- resolveOnTargetChemblIds(assessmentUniprot),
239
- resolveFamilySiblingUniprots(assessmentUniprot || geneSymbol),
238
+ resolveOnTargetChemblIds(assessmentUniprot, logger),
239
+ resolveFamilySiblingUniprots(assessmentUniprot || geneSymbol, logger),
240
240
  ]);
241
241
  const offTargetPanel = aggregateOffTargetPanel(phase2, fanout, assessmentUniprot, geneSymbol, familyComplexesBundle, onTargetChemblIds);
242
242
  if (offTargetPanel) {