@inflexa-ai/harness 0.30.3 → 0.31.1

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 (115) 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/prompts/briefing.d.ts +12 -1
  8. package/dist/prompts/briefing.js +27 -1
  9. package/dist/prompts/sandbox-standards.d.ts +1 -1
  10. package/dist/prompts/sandbox-standards.js +27 -0
  11. package/dist/sandbox/thread-env.d.ts +18 -12
  12. package/dist/sandbox/thread-env.js +36 -25
  13. package/dist/tools/bio/chembl.js +4 -3
  14. package/dist/tools/bio/drug-gene-interactions.d.ts +2 -14
  15. package/dist/tools/bio/drug-gene-interactions.js +10 -18
  16. package/dist/tools/bio/gene-disease-evidence.d.ts +3 -1
  17. package/dist/tools/bio/gene-disease-evidence.js +12 -9
  18. package/dist/tools/bio/keys.js +1 -0
  19. package/dist/tools/bio/lookup-annotation.d.ts +43 -0
  20. package/dist/tools/bio/lookup-annotation.js +19 -4
  21. package/dist/tools/bio/pubchem.js +5 -3
  22. package/dist/tools/bio/search-clinical-trials.js +5 -4
  23. package/dist/tools/bio/search-geo-datasets.d.ts +39 -9
  24. package/dist/tools/bio/search-geo-datasets.js +38 -20
  25. package/dist/tools/lib/api-utils.d.ts +27 -2
  26. package/dist/tools/lib/api-utils.js +43 -3
  27. package/dist/tools/lib/bgee-client.d.ts +16 -0
  28. package/dist/tools/lib/bgee-client.js +16 -5
  29. package/dist/tools/lib/cbioportal-client.d.ts +20 -0
  30. package/dist/tools/lib/cbioportal-client.js +16 -8
  31. package/dist/tools/lib/chembl-client.d.ts +105 -3
  32. package/dist/tools/lib/chembl-client.js +196 -84
  33. package/dist/tools/lib/clinical-benchmarks-client.d.ts +5 -0
  34. package/dist/tools/lib/clinical-benchmarks-client.js +5 -0
  35. package/dist/tools/lib/clinical-trials-client.d.ts +131 -2
  36. package/dist/tools/lib/clinical-trials-client.js +32 -6
  37. package/dist/tools/lib/clinvar-client.d.ts +35 -0
  38. package/dist/tools/lib/clinvar-client.js +34 -28
  39. package/dist/tools/lib/ctx-ops.d.ts +106 -35
  40. package/dist/tools/lib/ctx-ops.js +135 -69
  41. package/dist/tools/lib/dgidb-client.d.ts +73 -0
  42. package/dist/tools/lib/dgidb-client.js +25 -13
  43. package/dist/tools/lib/disgenet-client.d.ts +117 -24
  44. package/dist/tools/lib/disgenet-client.js +127 -34
  45. package/dist/tools/lib/disgenet-config.d.ts +1 -1
  46. package/dist/tools/lib/disgenet-config.js +5 -3
  47. package/dist/tools/lib/drugbank-client.d.ts +57 -58
  48. package/dist/tools/lib/drugbank-client.js +69 -64
  49. package/dist/tools/lib/drugbank-config.d.ts +6 -1
  50. package/dist/tools/lib/drugbank-config.js +6 -1
  51. package/dist/tools/lib/ema-client.d.ts +53 -0
  52. package/dist/tools/lib/ema-client.js +19 -7
  53. package/dist/tools/lib/ensembl-client.d.ts +4 -0
  54. package/dist/tools/lib/ensembl-client.js +4 -0
  55. package/dist/tools/lib/gwas-catalog-client.d.ts +64 -1
  56. package/dist/tools/lib/gwas-catalog-client.js +21 -6
  57. package/dist/tools/lib/identifier-resolver.d.ts +17 -1
  58. package/dist/tools/lib/identifier-resolver.js +13 -4
  59. package/dist/tools/lib/impc-client.d.ts +4 -0
  60. package/dist/tools/lib/impc-client.js +4 -0
  61. package/dist/tools/lib/iuphar-client.d.ts +10 -5
  62. package/dist/tools/lib/iuphar-client.js +49 -13
  63. package/dist/tools/lib/monarch-client.d.ts +4 -0
  64. package/dist/tools/lib/monarch-client.js +4 -0
  65. package/dist/tools/lib/openfda-client.d.ts +34 -0
  66. package/dist/tools/lib/openfda-client.js +22 -8
  67. package/dist/tools/lib/opentargets-client.d.ts +99 -4
  68. package/dist/tools/lib/opentargets-client.js +97 -37
  69. package/dist/tools/lib/pathway-client.d.ts +45 -0
  70. package/dist/tools/lib/pathway-client.js +88 -8
  71. package/dist/tools/lib/pharmgkb-client.d.ts +35 -1
  72. package/dist/tools/lib/pharmgkb-client.js +22 -6
  73. package/dist/tools/lib/pharmgkb-config.d.ts +8 -1
  74. package/dist/tools/lib/pharmgkb-config.js +8 -1
  75. package/dist/tools/lib/pubchem-client.d.ts +22 -0
  76. package/dist/tools/lib/pubchem-client.js +36 -33
  77. package/dist/tools/lib/pubchem-ops.d.ts +85 -5
  78. package/dist/tools/lib/pubchem-ops.js +96 -68
  79. package/dist/tools/lib/pubmed-client.d.ts +4 -0
  80. package/dist/tools/lib/pubmed-client.js +4 -0
  81. package/dist/tools/lib/string-client.d.ts +52 -0
  82. package/dist/tools/lib/string-client.js +12 -7
  83. package/dist/tools/lib/uniprot-client.d.ts +4 -0
  84. package/dist/tools/lib/uniprot-client.js +4 -0
  85. package/dist/tools/research/context7-docs.d.ts +23 -0
  86. package/dist/tools/research/context7-docs.js +49 -9
  87. package/dist/tools/research/search-github-repos.d.ts +4 -0
  88. package/dist/tools/research/search-github-repos.js +4 -0
  89. package/dist/workflows/execute-target-assessment.js +4 -4
  90. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +2 -1
  91. package/dist/workflows/target-assessment/assemblers/orchestrator.js +3 -3
  92. package/dist/workflows/target-assessment/collectors/index.d.ts +8 -2
  93. package/dist/workflows/target-assessment/collectors/index.js +37 -10
  94. package/dist/workflows/target-assessment/fanout/index.d.ts +2 -1
  95. package/dist/workflows/target-assessment/fanout/index.js +2 -2
  96. package/dist/workflows/target-assessment/lib/safety-corroboration.js +2 -2
  97. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +3 -2
  98. package/dist/workflows/target-assessment/lib/target-identity-filter.js +16 -5
  99. package/dist/workflows/target-assessment/phase4-assemble.d.ts +61 -60
  100. package/dist/workflows/target-assessment/phase4-assemble.js +2 -2
  101. package/dist/workflows/target-assessment/schemas.d.ts +2 -8
  102. package/dist/workflows/target-assessment/schemas.js +8 -2
  103. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +1 -4
  104. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -4
  105. package/package.json +1 -1
  106. package/dist/agents/sandbox/__fixtures__/deps.d.ts +0 -23
  107. package/dist/agents/sandbox/__fixtures__/deps.js +0 -102
  108. package/dist/citations/__fixtures__/resolver.d.ts +0 -3
  109. package/dist/citations/__fixtures__/resolver.js +0 -9
  110. package/dist/loop/__fixtures__/scripted-provider.d.ts +0 -31
  111. package/dist/loop/__fixtures__/scripted-provider.js +0 -81
  112. package/dist/providers/__fixtures__/session.d.ts +0 -17
  113. package/dist/providers/__fixtures__/session.js +0 -20
  114. package/dist/tools/__fixtures__/tool-context.d.ts +0 -13
  115. package/dist/tools/__fixtures__/tool-context.js +0 -22
@@ -10,20 +10,32 @@
10
10
  * panels never list a sibling receptor-complex as a developable hit.
11
11
  */
12
12
  import { z } from "zod";
13
- import { apiFetchValidated, describeApiError } from "./api-utils.js";
13
+ import { createNoopLogger } from "../../lib/console-logger.js";
14
+ import { apiFetchValidated, describeApiError, isUnexpectedApiError } from "./api-utils.js";
14
15
  import { IUPHAR_BASE, IUPHAR_HEADERS as HEADERS } from "./iuphar-config.js";
15
- // Validated at the fetch boundary. GtoPdb returns familyIds/complexIds as
16
- // arrays on every target, and the resolver reads `.length` / iterates them
17
- // without a guard, so they stay required — a missing value surfaces as
18
- // `invalid_response` rather than a runtime TypeError. subunitIds and type are
19
- // only read behind a guard, so they may be absent.
20
- const IupharTargetSchema = z.object({
16
+ // Validated at the fetch boundary. GtoPdb serves the target record in three
17
+ // wire shapes under this one schema, and the key set differs per shape:
18
+ //
19
+ // Shape A — the `/targets?…` list and `/targets/{id}/complexes`. It OMITS
20
+ // `complexIds`, and `type` is lower snake case (`gpcr`,
21
+ // `catalytic_receptor`, and `""` for an accessory protein).
22
+ // Shape B — `/targets/{id}/subunits`. It carries `complexIds` as a populated
23
+ // number array, and `type` is CamelCase (`GPCR`, `AccessoryProtein`).
24
+ // Shape C — `/targets/{id}`. Same key set as shape B, plus about 27 flat
25
+ // cross-reference keys, but `type` is the shape A casing again.
26
+ //
27
+ // Thus `complexIds` is optional, and its one read site in
28
+ // `getFamilyHeterodimers` guards the omission. `familyIds` stays required,
29
+ // because every shape sends it and an accessory protein sends `[]`. A missing
30
+ // `familyIds` surfaces as `invalid_response` rather than as a runtime
31
+ // TypeError.
32
+ export const IupharTargetSchema = z.object({
21
33
  targetId: z.number(),
22
34
  name: z.string(),
23
35
  type: z.string().nullable().optional(),
24
36
  familyIds: z.array(z.number()),
25
37
  subunitIds: z.array(z.number()).optional(),
26
- complexIds: z.array(z.number()),
38
+ complexIds: z.array(z.number()).optional(),
27
39
  subunitType: z.string().optional(),
28
40
  });
29
41
  const UNIPROT_ACCESSION_RE = /^[A-Z][0-9][A-Z0-9]{3}[0-9]$/;
@@ -93,7 +105,11 @@ export async function getSubunits(complexId) {
93
105
  */
94
106
  export async function getFamilyHeterodimers(uniprotOrGeneSymbol) {
95
107
  const target = await resolveTarget(uniprotOrGeneSymbol);
96
- if (!target || target.complexIds.length === 0)
108
+ // `resolveTarget` gives a shape A record, which omits `complexIds`. An
109
+ // omission thus says nothing about the complexes of the target, and only a
110
+ // present and empty array proves that there is none. `?.length === 0` is
111
+ // false for the omission, thus the `/complexes` call below answers for it.
112
+ if (!target || target.complexIds?.length === 0)
97
113
  return [];
98
114
  const url = `${IUPHAR_BASE}/targets/${target.targetId}/complexes`;
99
115
  const res = await apiFetchValidated(url, z.array(IupharTargetSchema), { headers: HEADERS });
@@ -112,6 +128,12 @@ export async function getFamilyHeterodimers(uniprotOrGeneSymbol) {
112
128
  const complex = batch[j];
113
129
  const subunits = subunitLists[j];
114
130
  const cleanedSubunits = subunits.map((s) => ({ ...s, name: stripHtml(s.name) }));
131
+ // Only a shape B row reaches this filter, because `getSubunits`
132
+ // calls `/targets/{id}/subunits`. Shape B is the one shape that
133
+ // names the kind of a target as `AccessoryProtein`, and the
134
+ // lower-case compare matches that CamelCase form. Shape A and shape
135
+ // C send `""` for the same target, thus the same filter over a list
136
+ // row or a single-target row would match nothing.
115
137
  const accessories = cleanedSubunits.filter((s) => (s.type ?? "").toLowerCase() === "accessoryprotein");
116
138
  results.push({
117
139
  complex: { ...complex, name: stripHtml(complex.name) },
@@ -143,7 +165,7 @@ export async function getAccessoryProteinNames(uniprotOrGeneSymbol) {
143
165
  }
144
166
  // getFamilySiblingUniprots iterates `targetIds` without a guard, so it stays
145
167
  // required; the other fields are not read, so they may be absent.
146
- const IupharFamilySchema = z.object({
168
+ export const IupharFamilySchema = z.object({
147
169
  familyId: z.number().optional(),
148
170
  name: z.string().optional(),
149
171
  targetIds: z.array(z.number()),
@@ -158,9 +180,15 @@ export async function getFamily(familyId) {
158
180
  return null;
159
181
  throw new Error(describeApiError(res.error));
160
182
  }
183
+ // GtoPdb answers an unknown family with HTTP 200 and the sentinel record
184
+ // `{familyId: -1, targetIds: []}`, never with a 404. Thus the sentinel is
185
+ // the one signal that the family does not exist, and the `notFound` branch
186
+ // above cannot report it.
187
+ if (res.value.familyId === -1)
188
+ return null;
161
189
  return res.value;
162
190
  }
163
- const IupharDatabaseLinkSchema = z.object({
191
+ export const IupharDatabaseLinkSchema = z.object({
164
192
  accession: z.string().optional(),
165
193
  database: z.string().optional(),
166
194
  species: z.string().optional(),
@@ -193,7 +221,8 @@ export async function getTargetHumanUniprots(targetId) {
193
221
  * CALCRL accession; the AMY1/2/3 complexes are excluded because they are
194
222
  * heterodimers, not separate primary proteins.
195
223
  */
196
- export async function getFamilySiblingUniprots(uniprotOrGeneSymbol) {
224
+ export async function getFamilySiblingUniprots(uniprotOrGeneSymbol, logger) {
225
+ const log = (logger ?? createNoopLogger()).named("iuphar-client.family-siblings").with({ query: uniprotOrGeneSymbol });
197
226
  const self = await resolveTarget(uniprotOrGeneSymbol);
198
227
  if (!self)
199
228
  return [];
@@ -221,8 +250,15 @@ export async function getFamilySiblingUniprots(uniprotOrGeneSymbol) {
221
250
  const uniprotLists = await Promise.all(batch.map(async (tid) => {
222
251
  const url = `${IUPHAR_BASE}/targets/${tid}`;
223
252
  const res = await apiFetchValidated(url, IupharTargetSchema, { headers: HEADERS });
224
- if (res.isErr())
253
+ if (res.isErr()) {
254
+ // A sibling that GtoPdb does not serve is a normal miss, and the
255
+ // loop drops it. But a contract break drops every sibling in the
256
+ // same way, thus an unexpected cause reaches the Logger first.
257
+ if (isUnexpectedApiError(res.error)) {
258
+ log.error("sibling target dropped from the result", { targetId: tid, cause: describeApiError(res.error) });
259
+ }
225
260
  return [];
261
+ }
226
262
  const t = res.value;
227
263
  if (t.subunitIds && t.subunitIds.length > 0)
228
264
  return [];
@@ -11,6 +11,10 @@
11
11
  * rather than by matching prose.
12
12
  *
13
13
  * The API is keyless and public.
14
+ *
15
+ * Absence policy: the OpenAPI document of Monarch declares an absent value as
16
+ * `anyOf` with `null`, thus the API sends an explicit `null`. A maybe-absent
17
+ * field carries `.nullish()` exactly.
14
18
  */
15
19
  /** A curated human phenotype attributed to variation in the gene. */
16
20
  export interface MonarchPhenotypeAssociation {
@@ -11,6 +11,10 @@
11
11
  * rather than by matching prose.
12
12
  *
13
13
  * The API is keyless and public.
14
+ *
15
+ * Absence policy: the OpenAPI document of Monarch declares an absent value as
16
+ * `anyOf` with `null`, thus the API sends an explicit `null`. A maybe-absent
17
+ * field carries `.nullish()` exactly.
14
18
  */
15
19
  import { z } from "zod";
16
20
  import { apiFetchValidated, describeApiError, isUnexpectedApiError } from "./api-utils.js";
@@ -3,7 +3,12 @@
3
3
  *
4
4
  * Used by §2.6.2 (FAERS summary) and §2.6.6 (class precedent — drugs-in-class
5
5
  * AEs).
6
+ *
7
+ * Absence policy: openFDA omits the key of an absent value, and the `openfda`
8
+ * object can arrive as `{}`. Thus a maybe-absent field carries `.optional()`,
9
+ * not `.nullable()`.
6
10
  */
11
+ import { z } from "zod";
7
12
  export interface AdverseEventCount {
8
13
  reaction: string;
9
14
  count: number;
@@ -42,6 +47,35 @@ export declare function getFaersByDrug(drugName: string, options?: {
42
47
  * and §2.6.8 risk summary.
43
48
  */
44
49
  export declare function getFaersSeriousness(drugName: string): Promise<SeriousnessProfile | null>;
50
+ export declare const OpenFdaLabelResponseSchema: z.ZodObject<{
51
+ results: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodObject<{
52
+ set_id: z.ZodOptional<z.ZodUnknown>;
53
+ openfda: z.ZodOptional<z.ZodObject<{
54
+ application_number: z.ZodOptional<z.ZodUnknown>;
55
+ brand_name: z.ZodOptional<z.ZodUnknown>;
56
+ generic_name: z.ZodOptional<z.ZodUnknown>;
57
+ }, z.core.$strip>>;
58
+ effective_time: z.ZodOptional<z.ZodUnknown>;
59
+ boxed_warning: z.ZodOptional<z.ZodUnknown>;
60
+ warnings_and_cautions: z.ZodOptional<z.ZodUnknown>;
61
+ warnings: z.ZodOptional<z.ZodUnknown>;
62
+ spl_medguide: z.ZodOptional<z.ZodArray<z.ZodString>>;
63
+ patient_medication_information: z.ZodOptional<z.ZodArray<z.ZodString>>;
64
+ }, z.core.$strip>, z.ZodTransform<DrugLabelAction, {
65
+ set_id?: unknown;
66
+ openfda?: {
67
+ application_number?: unknown;
68
+ brand_name?: unknown;
69
+ generic_name?: unknown;
70
+ } | undefined;
71
+ effective_time?: unknown;
72
+ boxed_warning?: unknown;
73
+ warnings_and_cautions?: unknown;
74
+ warnings?: unknown;
75
+ spl_medguide?: string[] | undefined;
76
+ patient_medication_information?: string[] | undefined;
77
+ }>>>>;
78
+ }, z.core.$strip>;
45
79
  /**
46
80
  * Fetch FDA Structured Product Label entries by drug name. The name matches
47
81
  * the generic name or the brand name, thus a caller that holds only a brand
@@ -3,6 +3,10 @@
3
3
  *
4
4
  * Used by §2.6.2 (FAERS summary) and §2.6.6 (class precedent — drugs-in-class
5
5
  * AEs).
6
+ *
7
+ * Absence policy: openFDA omits the key of an absent value, and the `openfda`
8
+ * object can arrive as `{}`. Thus a maybe-absent field carries `.optional()`,
9
+ * not `.nullable()`.
6
10
  */
7
11
  import { z } from "zod";
8
12
  import { apiFetchValidated, describeApiError } from "./api-utils.js";
@@ -22,7 +26,13 @@ const OpenFdaLabelMetaSchema = z.object({
22
26
  brand_name: z.unknown().optional(),
23
27
  generic_name: z.unknown().optional(),
24
28
  });
25
- /** A single openFDA structured-product-label record (raw wire shape). */
29
+ /**
30
+ * A single openFDA structured-product-label record (raw wire shape).
31
+ *
32
+ * `spl_medguide` and `patient_medication_information` are the two label sections
33
+ * that carry the patient-facing safety documents. The field reference of the
34
+ * provider types both as an array of string.
35
+ */
26
36
  const OpenFdaLabelSchema = z.object({
27
37
  set_id: z.unknown().optional(),
28
38
  openfda: OpenFdaLabelMetaSchema.optional(),
@@ -30,9 +40,8 @@ const OpenFdaLabelSchema = z.object({
30
40
  boxed_warning: z.unknown().optional(),
31
41
  warnings_and_cautions: z.unknown().optional(),
32
42
  warnings: z.unknown().optional(),
33
- rems_summary: z.unknown().optional(),
34
- rems_indication: z.unknown().optional(),
35
- medication_guide: z.unknown().optional(),
43
+ spl_medguide: z.array(z.string()).optional(),
44
+ patient_medication_information: z.array(z.string()).optional(),
36
45
  });
37
46
  /**
38
47
  * Escape a value for embedding inside a double-quoted openFDA (Lucene) phrase.
@@ -166,8 +175,12 @@ function buildLabelSourceUrl(label) {
166
175
  }
167
176
  function mapLabel(raw) {
168
177
  const openfda = raw?.openfda ?? {};
169
- const remsTextCandidates = [raw?.rems_summary, raw?.rems_indication, raw?.medication_guide];
170
- const hasRems = remsTextCandidates.some((v) => joinSection(v) !== null) || (joinSection(raw?.boxed_warning)?.toLowerCase().includes("rems") ?? false);
178
+ // The label reference carries no REMS field, thus the signal is the word
179
+ // itself in one of the three sections that a REMS program writes to. The
180
+ // presence of a medication guide alone is not the signal: many labels with no
181
+ // REMS carry one, and a presence test would report a REMS that does not exist.
182
+ const remsTextCandidates = [raw?.boxed_warning, raw?.spl_medguide, raw?.patient_medication_information];
183
+ const hasRems = remsTextCandidates.some((v) => joinSection(v)?.toLowerCase().includes("rems") ?? false);
171
184
  return {
172
185
  applicationNumber: firstString(openfda.application_number),
173
186
  brandName: firstString(openfda.brand_name),
@@ -181,8 +194,9 @@ function mapLabel(raw) {
181
194
  }
182
195
  // The label endpoint's records are mapped to `DrugLabelAction` rows by
183
196
  // `mapLabel` — a context-free normalize folded into the schema so ONE schema
184
- // validates the raw wire AND emits the output rows.
185
- const OpenFdaLabelResponseSchema = z.object({
197
+ // validates the raw wire AND emits the output rows. It is exported so that the
198
+ // golden-fixture table drives it.
199
+ export const OpenFdaLabelResponseSchema = z.object({
186
200
  results: z.array(OpenFdaLabelSchema.transform(mapLabel)).optional(),
187
201
  });
188
202
  /**
@@ -2,7 +2,12 @@
2
2
  * Pure async client functions for the Open Targets Platform GraphQL API.
3
3
  *
4
4
  * Used directly by target-assessment workflow steps and by tool wrappers.
5
+ *
6
+ * Absence policy: the nullability of the GraphQL SDL encodes an absent value.
7
+ * Open Targets answers each requested field with a key and an explicit `null`,
8
+ * thus the SDL gives each modifier and `.optional()` is wrong.
5
9
  */
10
+ import { z } from "zod";
6
11
  export interface Association {
7
12
  diseaseId: string;
8
13
  diseaseName: string;
@@ -50,6 +55,13 @@ export interface BaselineExpressionEntry {
50
55
  tissueId: string;
51
56
  tissueLabel: string;
52
57
  organSystem: string | null;
58
+ /**
59
+ * The Open Targets datasource of the row, for example `gtex`,
60
+ * `tabula_sapiens` or `DICE`. One target carries rows of more than one
61
+ * datasource, and each datasource has its own unit and its own resolution.
62
+ * Thus a consumer that compares two entries must first compare this value.
63
+ */
64
+ datasourceId: string;
53
65
  rna: {
54
66
  value: number;
55
67
  unit: string;
@@ -58,6 +70,82 @@ export interface BaselineExpressionEntry {
58
70
  level: number | null;
59
71
  } | null;
60
72
  }
73
+ export declare const TargetAssociationsDataSchema: z.ZodObject<{
74
+ target: z.ZodOptional<z.ZodNullable<z.ZodObject<{
75
+ id: z.ZodString;
76
+ approvedSymbol: z.ZodString;
77
+ approvedName: z.ZodString;
78
+ tractability: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
79
+ label: z.ZodOptional<z.ZodString>;
80
+ modality: z.ZodOptional<z.ZodString>;
81
+ value: z.ZodOptional<z.ZodBoolean>;
82
+ }, z.core.$strip>>>>;
83
+ }, z.core.$strip>>>;
84
+ associations: z.ZodOptional<z.ZodNullable<z.ZodObject<{
85
+ associatedDiseases: z.ZodOptional<z.ZodObject<{
86
+ rows: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
87
+ disease: z.ZodObject<{
88
+ id: z.ZodString;
89
+ name: z.ZodString;
90
+ }, z.core.$strip>;
91
+ score: z.ZodNumber;
92
+ datatypeScores: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
93
+ id: z.ZodOptional<z.ZodString>;
94
+ score: z.ZodOptional<z.ZodNumber>;
95
+ }, z.core.$strip>>>>;
96
+ }, z.core.$strip>>>>;
97
+ }, z.core.$strip>>;
98
+ }, z.core.$strip>>>;
99
+ }, z.core.$strip>;
100
+ export declare const TargetSafetyDataSchema: z.ZodObject<{
101
+ target: z.ZodOptional<z.ZodNullable<z.ZodObject<{
102
+ id: z.ZodOptional<z.ZodString>;
103
+ approvedSymbol: z.ZodString;
104
+ safetyLiabilities: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
105
+ event: z.ZodOptional<z.ZodNullable<z.ZodString>>;
106
+ biosamples: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
107
+ tissueLabel: z.ZodOptional<z.ZodNullable<z.ZodString>>;
108
+ }, z.core.$strip>>>>;
109
+ effects: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
110
+ direction: z.ZodOptional<z.ZodNullable<z.ZodString>>;
111
+ }, z.core.$strip>>>>;
112
+ datasource: z.ZodOptional<z.ZodNullable<z.ZodString>>;
113
+ }, z.core.$strip>>>>;
114
+ }, z.core.$strip>>>;
115
+ }, z.core.$strip>;
116
+ /**
117
+ * `Target.baselineExpression`, per the SDL.
118
+ *
119
+ * The modifiers come from the SDL and from nothing else: `BaselineExpression!`
120
+ * and its `count`, `rows`, `unit`, `datasourceId` and `targetId` are non-null,
121
+ * the five quantile fields are `Float` and thus nullable, and each biosample
122
+ * link is a nullable `Biosample`.
123
+ */
124
+ export declare const TargetExpressionDataSchema: z.ZodObject<{
125
+ target: z.ZodNullable<z.ZodObject<{
126
+ baselineExpression: z.ZodObject<{
127
+ count: z.ZodNumber;
128
+ rows: z.ZodArray<z.ZodObject<{
129
+ unit: z.ZodString;
130
+ median: z.ZodNullable<z.ZodNumber>;
131
+ q1: z.ZodNullable<z.ZodNumber>;
132
+ q3: z.ZodNullable<z.ZodNumber>;
133
+ min: z.ZodNullable<z.ZodNumber>;
134
+ max: z.ZodNullable<z.ZodNumber>;
135
+ datasourceId: z.ZodString;
136
+ targetId: z.ZodString;
137
+ tissueBiosample: z.ZodNullable<z.ZodObject<{
138
+ biosampleId: z.ZodString;
139
+ biosampleName: z.ZodString;
140
+ }, z.core.$strip>>;
141
+ tissueBiosampleParent: z.ZodNullable<z.ZodObject<{
142
+ biosampleId: z.ZodString;
143
+ biosampleName: z.ZodString;
144
+ }, z.core.$strip>>;
145
+ }, z.core.$strip>>;
146
+ }, z.core.$strip>;
147
+ }, z.core.$strip>>;
148
+ }, z.core.$strip>;
61
149
  /** Fetch target info, tractability, and disease associations for an Ensembl gene id. */
62
150
  export declare function searchTargetAssociations(ensemblId: string, limit?: number): Promise<TargetInfo | null>;
63
151
  /** Fetch target associations for a disease (EFO id). */
@@ -77,8 +165,15 @@ export declare function getTargetSafetyLiabilities(ensemblId: string): Promise<{
77
165
  safetyLiabilities: SafetyLiability[];
78
166
  } | null>;
79
167
  /**
80
- * Fetch baseline RNA/protein expression across tissues. Open Targets
81
- * exposes per-tissue expression with organ system tags — used by §2.7
82
- * (Off-Tissue Risk) and §3.9 (Normal Tissue Expression).
168
+ * Map one `baselineExpression` page onto the baseline-expression entries.
169
+ *
170
+ * The function is pure, thus the golden-fixture table exercises it against a
171
+ * stored payload.
172
+ */
173
+ export declare function mapBaselineExpression(data: z.infer<typeof TargetExpressionDataSchema>): BaselineExpressionEntry[];
174
+ /**
175
+ * Fetch baseline RNA expression across tissues. Open Targets gives one row for
176
+ * each tissue and datasource, with the parent biosample as the organ system —
177
+ * used by §2.7 (Off-Tissue Risk) and §3.9 (Normal Tissue Expression).
83
178
  */
84
- export declare function getBaselineExpression(ensemblId: string): Promise<BaselineExpressionEntry[]>;
179
+ export declare function getBaselineExpression(ensemblId: string, limit?: number): Promise<BaselineExpressionEntry[]>;
@@ -2,6 +2,10 @@
2
2
  * Pure async client functions for the Open Targets Platform GraphQL API.
3
3
  *
4
4
  * Used directly by target-assessment workflow steps and by tool wrappers.
5
+ *
6
+ * Absence policy: the nullability of the GraphQL SDL encodes an absent value.
7
+ * Open Targets answers each requested field with a key and an explicit `null`,
8
+ * thus the SDL gives each modifier and `.optional()` is wrong.
5
9
  */
6
10
  import { z } from "zod";
7
11
  import { apiFetchValidated, describeApiError } from "./api-utils.js";
@@ -79,22 +83,36 @@ const SAFETY_QUERY = `
79
83
  }
80
84
  `;
81
85
  const EXPRESSION_QUERY = `
82
- query TargetExpression($ensemblId: String!) {
86
+ query TargetBaselineExpression($ensemblId: String!, $size: Int!) {
83
87
  target(ensemblId: $ensemblId) {
84
88
  id
85
89
  approvedSymbol
86
- expressions {
87
- tissue {
88
- id
89
- label
90
- organs
90
+ baselineExpression(page: { size: $size, index: 0 }) {
91
+ count
92
+ rows {
93
+ unit
94
+ median
95
+ q1
96
+ q3
97
+ min
98
+ max
99
+ datasourceId
100
+ targetId
101
+ tissueBiosample { biosampleId biosampleName }
102
+ tissueBiosampleParent { biosampleId biosampleName }
91
103
  }
92
- rna { value unit }
93
- protein { level }
94
104
  }
95
105
  }
96
106
  }
97
107
  `;
108
+ /**
109
+ * The page size of one `baselineExpression` request.
110
+ *
111
+ * `Pagination.size` caps at 3000 in the SDL, and one target carries more than
112
+ * 1400 rows. Thus a smaller page drops tissues, and it gives no signal that it
113
+ * dropped them.
114
+ */
115
+ const BASELINE_EXPRESSION_PAGE_SIZE = 3000;
98
116
  // Open Targets GraphQL `data` payload schemas, validated at the fetch boundary.
99
117
  // Fields the mapping code reads behind a guard (optional chaining, `?? default`,
100
118
  // `Array.isArray`) are `.nullable().optional()`; fields read directly — a missing
@@ -104,7 +122,7 @@ const EXPRESSION_QUERY = `
104
122
  // that resolves empty — an unknown/retired id yields `{"target": null}`, which a
105
123
  // bare `.optional()` rejects, turning a clean "not found" into a thrown error.
106
124
  const DatatypeScoreSchema = z.object({ id: z.string().optional(), score: z.number().optional() });
107
- const TargetAssociationsDataSchema = z.object({
125
+ export const TargetAssociationsDataSchema = z.object({
108
126
  target: z
109
127
  .object({
110
128
  id: z.string(),
@@ -175,7 +193,7 @@ const DiseaseSearchDataSchema = z.object({
175
193
  .nullable()
176
194
  .optional(),
177
195
  });
178
- const TargetSafetyDataSchema = z.object({
196
+ export const TargetSafetyDataSchema = z.object({
179
197
  target: z
180
198
  .object({
181
199
  id: z.string().optional(),
@@ -199,35 +217,57 @@ const TargetSafetyDataSchema = z.object({
199
217
  .nullable()
200
218
  .optional(),
201
219
  });
202
- const TargetExpressionDataSchema = z.object({
220
+ /** `Biosample`, as `BaselineExpressionRow` embeds it. Both leaf fields are non-null in the SDL. */
221
+ const BiosampleSchema = z.object({ biosampleId: z.string(), biosampleName: z.string() });
222
+ /**
223
+ * `Target.baselineExpression`, per the SDL.
224
+ *
225
+ * The modifiers come from the SDL and from nothing else: `BaselineExpression!`
226
+ * and its `count`, `rows`, `unit`, `datasourceId` and `targetId` are non-null,
227
+ * the five quantile fields are `Float` and thus nullable, and each biosample
228
+ * link is a nullable `Biosample`.
229
+ */
230
+ export const TargetExpressionDataSchema = z.object({
203
231
  target: z
204
232
  .object({
205
- expressions: z
206
- .array(z.object({
207
- tissue: z.object({ id: z.string(), label: z.string(), organs: z.array(z.string()).nullable().optional() }),
208
- rna: z.object({ value: z.number().nullable().optional(), unit: z.string().nullable().optional() }).nullable().optional(),
209
- protein: z.object({ level: z.number().nullable().optional() }).nullable().optional(),
210
- }))
211
- .nullable()
212
- .optional(),
233
+ baselineExpression: z.object({
234
+ count: z.number(),
235
+ rows: z.array(z.object({
236
+ unit: z.string(),
237
+ median: z.number().nullable(),
238
+ q1: z.number().nullable(),
239
+ q3: z.number().nullable(),
240
+ min: z.number().nullable(),
241
+ max: z.number().nullable(),
242
+ datasourceId: z.string(),
243
+ targetId: z.string(),
244
+ tissueBiosample: BiosampleSchema.nullable(),
245
+ tissueBiosampleParent: BiosampleSchema.nullable(),
246
+ })),
247
+ }),
213
248
  })
214
- .nullable()
215
- .optional(),
249
+ .nullable(),
216
250
  });
217
251
  function extractDatatype(datatypeScores, id) {
218
252
  const match = datatypeScores.find((d) => d.id === id);
219
253
  return match?.score ?? null;
220
254
  }
221
255
  async function gqlFetch(query, variables, schema) {
222
- const res = await apiFetchValidated(OT_GRAPHQL, z.object({ data: schema.optional() }), {
256
+ const res = await apiFetchValidated(OT_GRAPHQL, z.object({ data: schema.optional(), errors: z.array(z.object({ message: z.string().optional() })).optional() }), {
223
257
  method: "POST",
224
258
  headers: OT_HEADERS,
225
259
  body: JSON.stringify({ query, variables }),
226
260
  });
227
261
  if (res.isErr())
228
262
  throw new Error(describeApiError(res.error));
229
- if (!res.value.data)
230
- throw new Error("Open Targets returned no data");
263
+ if (!res.value.data) {
264
+ // A GraphQL server answers HTTP 200 with an `errors` array and no `data`
265
+ // key when the document itself is invalid, for example when it names a
266
+ // retired field. Without the first message the caller sees only "no
267
+ // data", which names no cause and points at no field.
268
+ const firstError = res.value.errors?.[0]?.message;
269
+ throw new Error(firstError ? `Open Targets returned no data: ${firstError}` : "Open Targets returned no data");
270
+ }
231
271
  return res.value.data;
232
272
  }
233
273
  /** Fetch target info, tractability, and disease associations for an Ensembl gene id. */
@@ -318,18 +358,38 @@ export async function getTargetSafetyLiabilities(ensemblId) {
318
358
  return { targetSymbol: target.approvedSymbol, safetyLiabilities };
319
359
  }
320
360
  /**
321
- * Fetch baseline RNA/protein expression across tissues. Open Targets
322
- * exposes per-tissue expression with organ system tags — used by §2.7
323
- * (Off-Tissue Risk) and §3.9 (Normal Tissue Expression).
361
+ * Map one `baselineExpression` page onto the baseline-expression entries.
362
+ *
363
+ * The function is pure, thus the golden-fixture table exercises it against a
364
+ * stored payload.
365
+ */
366
+ export function mapBaselineExpression(data) {
367
+ const entries = [];
368
+ for (const row of data.target?.baselineExpression.rows ?? []) {
369
+ // A row with no tissue biosample measures a cell type alone, thus it
370
+ // carries no tissue identity and it cannot become a tissue entry.
371
+ const tissue = row.tissueBiosample;
372
+ if (!tissue)
373
+ continue;
374
+ entries.push({
375
+ tissueId: tissue.biosampleId,
376
+ tissueLabel: tissue.biosampleName,
377
+ organSystem: row.tissueBiosampleParent?.biosampleName ?? null,
378
+ datasourceId: row.datasourceId,
379
+ rna: row.median === null ? null : { value: row.median, unit: row.unit },
380
+ // The `baselineExpression` surface carries no protein measurement,
381
+ // thus the protein level of an entry is always absent.
382
+ protein: null,
383
+ });
384
+ }
385
+ return entries;
386
+ }
387
+ /**
388
+ * Fetch baseline RNA expression across tissues. Open Targets gives one row for
389
+ * each tissue and datasource, with the parent biosample as the organ system —
390
+ * used by §2.7 (Off-Tissue Risk) and §3.9 (Normal Tissue Expression).
324
391
  */
325
- export async function getBaselineExpression(ensemblId) {
326
- const data = await gqlFetch(EXPRESSION_QUERY, { ensemblId }, TargetExpressionDataSchema);
327
- const expressions = data.target?.expressions ?? [];
328
- return expressions.map((e) => ({
329
- tissueId: e.tissue.id,
330
- tissueLabel: e.tissue.label,
331
- organSystem: Array.isArray(e.tissue.organs) && e.tissue.organs.length > 0 ? e.tissue.organs[0] : null,
332
- rna: e.rna && typeof e.rna.value === "number" ? { value: e.rna.value, unit: e.rna.unit ?? "TPM" } : null,
333
- protein: e.protein ? { level: typeof e.protein.level === "number" ? e.protein.level : null } : null,
334
- }));
392
+ export async function getBaselineExpression(ensemblId, limit = BASELINE_EXPRESSION_PAGE_SIZE) {
393
+ const data = await gqlFetch(EXPRESSION_QUERY, { ensemblId, size: limit }, TargetExpressionDataSchema);
394
+ return mapBaselineExpression(data);
335
395
  }
@@ -2,7 +2,12 @@
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
  */
10
+ import { z } from "zod";
6
11
  export declare function stripHtmlAndCollapseWs(s: string): string;
7
12
  export type PathwaySource = "kegg" | "reactome";
8
13
  export interface Pathway {
@@ -14,6 +19,25 @@ export interface Pathway {
14
19
  entity_uniprots?: string[];
15
20
  url?: string;
16
21
  }
22
+ export declare const ReactomeSearchResponseSchema: z.ZodObject<{
23
+ results: z.ZodOptional<z.ZodArray<z.ZodObject<{
24
+ entries: z.ZodOptional<z.ZodArray<z.ZodObject<{
25
+ stId: z.ZodOptional<z.ZodString>;
26
+ name: z.ZodOptional<z.ZodString>;
27
+ }, z.core.$strip>>>;
28
+ }, z.core.$strip>>>;
29
+ }, z.core.$strip>;
30
+ /** Participant list — `searchReactome` reads `displayName` per participant. */
31
+ export declare const ReactomeParticipantsSchema: z.ZodArray<z.ZodObject<{
32
+ displayName: z.ZodOptional<z.ZodString>;
33
+ }, z.core.$strip>>;
34
+ /** Participant list — `getPathwayMemberships` reads `refEntities` per participant. */
35
+ export declare const ReactomeParticipantEntitiesSchema: z.ZodArray<z.ZodObject<{
36
+ refEntities: z.ZodOptional<z.ZodArray<z.ZodObject<{
37
+ schemaClass: z.ZodOptional<z.ZodString>;
38
+ identifier: z.ZodOptional<z.ZodString>;
39
+ }, z.core.$strip>>>;
40
+ }, z.core.$strip>>;
17
41
  export interface PathwaySearchOptions {
18
42
  source?: "kegg" | "reactome" | "both";
19
43
  organism?: string;
@@ -21,6 +45,27 @@ export interface PathwaySearchOptions {
21
45
  maxResults?: number;
22
46
  }
23
47
  export declare function searchKegg(query: string, organism: string, maxResults: number, includeGenes: boolean): Promise<Pathway[]>;
48
+ /**
49
+ * Read the KEGG gene identifier of a symbol, for example `hsa:7157` for `TP53`.
50
+ *
51
+ * `find/{organism}/{symbol}` searches the gene names of one organism, and it
52
+ * ranks a partial name match above an exact one. A query for `TP53` puts
53
+ * `TP53BP2` first. Thus the caller matches the symbol against the alias list of
54
+ * each row, and it never takes the first row.
55
+ */
56
+ export declare function selectKeggGeneId(rows: string[][], symbol: string): string | null;
57
+ /** The pathway identifiers of a `link/pathway/{geneId}` answer, without the `path:` prefix. */
58
+ export declare function parseKeggPathwayLinks(rows: string[][]): string[];
59
+ /**
60
+ * The pathways that hold one gene, from KEGG.
61
+ *
62
+ * `find/pathway/{query}` matches a pathway NAME only, thus a gene symbol gives
63
+ * an empty body there. The membership path is a gene resolution, then
64
+ * `link/pathway/{geneId}`, then one `list/pathway/{organism}` call for the
65
+ * names. The one list call costs about 22 kB and names every pathway of the
66
+ * organism, thus it replaces one `get/{id}` call for each membership.
67
+ */
68
+ export declare function keggPathwaysForGene(geneSymbol: string, organism: string, maxResults: number): Promise<Pathway[]>;
24
69
  export declare function searchReactome(query: string, species: string, maxResults: number, includeGenes: boolean): Promise<Pathway[]>;
25
70
  export declare function searchPathways(query: string, options?: PathwaySearchOptions): Promise<Pathway[]>;
26
71
  /**