@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
@@ -1,91 +1,96 @@
1
1
  /**
2
- * Pure async client functions for DrugBank.
2
+ * Pure async client functions for the DrugBank Discovery API.
3
+ *
4
+ * CONTRACT UNVERIFIED (key-blocked, 2026-08-28). No key exists here, and an
5
+ * unauthenticated probe answers 401 before it routes, thus no live call proves
6
+ * a route or a record. The evidence is secondary: the Wayback captures of
7
+ * `docs.drugbank.com/discovery/v1` and the sample code of the provider. Refer to
8
+ * the truth map, file 07, section 1.
3
9
  *
4
10
  * Requires a DrugBank API key; `getDrugbankHeaders` throws when it is absent.
11
+ *
12
+ * Absence policy: no evidence establishes the encoding of an absent value,
13
+ * because no key exists for a live call and both fixtures are synthetic. Each
14
+ * maybe-absent field carries `.optional()` today, and no field carries
15
+ * `.nullable()`.
5
16
  */
6
17
  import { z } from "zod";
7
18
  import { apiFetchValidated, describeApiError } from "./api-utils.js";
8
19
  import { DRUGBANK_BASE, getDrugbankHeaders } from "./drugbank-config.js";
9
20
  /** Hard ceiling on each prose field at the parse boundary; callers trim further. */
10
21
  const PROSE_CEILING = 500;
11
- // A single schema that both validates and normalizes one DrugBank record.
12
- // The `.object(...)` half is the snake_case wire shape (every field optional —
13
- // the API omits absent values); the `.transform(...)` half maps it into the
14
- // camelCase result we return. Parsing IS the validation: `apiFetchValidated`
15
- // runs this schema over the JSON, so a payload whose field TYPES drift (an
16
- // object where the API used to send an array, a number for a string) is
17
- // rejected as `invalid_response` instead of being silently mis-mapped. Because
18
- // the schema carries the transform, `z.infer` below is the OUTPUT (camelCase)
19
- // type callers receive — there is no separate raw interface or mapper to keep
20
- // in sync.
22
+ /** The page ceiling of the `per_page` parameter. There is no `limit` parameter. */
23
+ const MAX_PER_PAGE = 50;
24
+ // A single schema that both validates and normalizes one Discovery drug record.
25
+ // The `.object(...)` half is the snake_case wire shape; the `.transform(...)`
26
+ // half maps it into the camelCase result we return. Parsing IS the validation:
27
+ // `apiFetchValidated` runs this schema over the JSON, so a payload whose field
28
+ // TYPES drift is rejected as `invalid_response` instead of being silently
29
+ // mis-mapped. Because the schema carries the transform, `z.infer` below is the
30
+ // OUTPUT (camelCase) type callers receive.
31
+ //
32
+ // `drugbank_id` is MANDATORY. It is the one key that every Discovery record
33
+ // carries, thus an error envelope such as `{"error":"Key invalid"}` fails the
34
+ // parse and the caller sees `invalid_response` rather than one blank drug row.
21
35
  const DrugResultSchema = z
22
36
  .object({
23
- drugbank_id: z.string().optional(),
37
+ drugbank_id: z.string(),
24
38
  name: z.string().optional(),
25
39
  description: z.string().optional(),
26
40
  type: z.string().optional(),
27
41
  groups: z.array(z.string()).optional(),
28
- categories: z.array(z.object({ category: z.string().optional() })).optional(),
29
- indication: z.string().optional(),
30
- pharmacodynamics: z.string().optional(),
31
- mechanism_of_action: z.string().optional(),
32
- toxicity: z.string().optional(),
33
- half_life: z.string().optional(),
34
- targets: z
35
- .array(z.object({
36
- name: z.string().optional(),
37
- gene_name: z.string().optional(),
38
- actions: z.array(z.string()).optional(),
39
- known_action: z.string().optional(),
40
- }))
41
- .optional(),
42
- drug_interactions: z
43
- .array(z.object({
44
- drugbank_id: z.string().optional(),
45
- name: z.string().optional(),
46
- description: z.string().optional(),
47
- }))
48
- .optional(),
49
42
  })
50
43
  .transform((raw) => ({
51
- drugbankId: raw.drugbank_id ?? "",
44
+ drugbankId: raw.drugbank_id,
52
45
  name: raw.name ?? "",
53
46
  description: (raw.description ?? "").slice(0, PROSE_CEILING),
54
47
  type: raw.type ?? "",
55
48
  groups: raw.groups ?? [],
56
- categories: (raw.categories ?? []).map((c) => c.category ?? "").filter(Boolean),
57
- indication: (raw.indication ?? "").slice(0, PROSE_CEILING),
58
- pharmacodynamics: (raw.pharmacodynamics ?? "").slice(0, PROSE_CEILING),
59
- mechanismOfAction: (raw.mechanism_of_action ?? "").slice(0, PROSE_CEILING),
60
- toxicity: (raw.toxicity ?? "").slice(0, PROSE_CEILING),
61
- halfLife: raw.half_life ?? "",
62
- targets: (raw.targets ?? []).map((t) => ({
63
- name: t.name ?? "",
64
- geneSymbol: t.gene_name ?? "",
65
- actions: t.actions ?? [],
66
- knownAction: t.known_action ?? "",
67
- })),
68
- interactions: (raw.drug_interactions ?? []).map((i) => ({
69
- drugbankId: i.drugbank_id ?? "",
70
- name: i.name ?? "",
71
- description: (i.description ?? "").slice(0, 200),
72
- })),
73
49
  }));
74
- // The /drugs endpoint returns a single object (by-id lookup) or an array
75
- // (query/target lookups); accept either, transforming each element.
76
- const DrugResponseSchema = z.union([DrugResultSchema, z.array(DrugResultSchema)]);
50
+ // The Discovery API wraps nothing: a list route answers with a bare JSON array,
51
+ // and a lookup route answers with a bare object. Accept either, and transform
52
+ // each element.
53
+ export const DrugResponseSchema = z.union([DrugResultSchema, z.array(DrugResultSchema)]);
54
+ // One bond of `/bonds/targets`. The row names the drug that binds the target,
55
+ // thus a gene query reaches the drugs through the bonds. The bond carries no
56
+ // drug record beyond the id and the name.
57
+ export const BondResponseSchema = z.array(z.object({
58
+ drug: z.object({ drugbank_id: z.string(), name: z.string().optional() }),
59
+ }));
60
+ /** Translate the internal limit into the `per_page` of the provider. */
61
+ function perPageOf(limit) {
62
+ return Math.min(Math.max(limit, 1), MAX_PER_PAGE);
63
+ }
64
+ /** The drugs that bind a target, keyed by the gene name of its polypeptides. */
65
+ async function searchDrugbankByTarget(headers, gene, limit) {
66
+ const query = encodeURIComponent(`polypeptides.gene_name:${gene}`);
67
+ const url = `${DRUGBANK_BASE}/bonds/targets?q=${query}&per_page=${perPageOf(limit)}`;
68
+ const res = await apiFetchValidated(url, BondResponseSchema, { headers });
69
+ if (res.isErr())
70
+ throw new Error(describeApiError(res.error));
71
+ // One drug binds a target through more than one bond, thus the rows repeat a
72
+ // drug. The map keeps the first row of each drug.
73
+ const drugs = new Map();
74
+ for (const bond of res.value) {
75
+ if (drugs.has(bond.drug.drugbank_id))
76
+ continue;
77
+ drugs.set(bond.drug.drugbank_id, {
78
+ drugbankId: bond.drug.drugbank_id,
79
+ name: bond.drug.name ?? "",
80
+ description: "",
81
+ type: "",
82
+ groups: [],
83
+ });
84
+ }
85
+ return [...drugs.values()].slice(0, limit);
86
+ }
77
87
  export async function searchDrugbank(apiKey, query, searchType, limit = 10) {
78
88
  const headers = getDrugbankHeaders(apiKey);
79
- let url;
80
- if (searchType === "target") {
81
- url = `${DRUGBANK_BASE}/drugs?target=${encodeURIComponent(query)}&limit=${limit}`;
82
- }
83
- else if (query.startsWith("DB")) {
84
- url = `${DRUGBANK_BASE}/drugs/${query}`;
85
- }
86
- else {
87
- url = `${DRUGBANK_BASE}/drugs?q=${encodeURIComponent(query)}&limit=${limit}`;
88
- }
89
+ if (searchType === "target")
90
+ return searchDrugbankByTarget(headers, query, limit);
91
+ const url = query.startsWith("DB")
92
+ ? `${DRUGBANK_BASE}/drugs/${encodeURIComponent(query)}`
93
+ : `${DRUGBANK_BASE}/drugs?q=${encodeURIComponent(query)}&per_page=${perPageOf(limit)}`;
89
94
  const res = await apiFetchValidated(url, DrugResponseSchema, { headers });
90
95
  if (res.isErr())
91
96
  throw new Error(describeApiError(res.error));
@@ -1,2 +1,7 @@
1
- export declare const DRUGBANK_BASE = "https://api.drugbank.com/v1";
1
+ /**
2
+ * The Discovery API of DrugBank. The Clinical API (`/v1`) serves no `?q=` search
3
+ * on its drug resource, thus only Discovery answers a name search and a target
4
+ * search.
5
+ */
6
+ export declare const DRUGBANK_BASE = "https://api.drugbank.com/discovery/v1";
2
7
  export declare function getDrugbankHeaders(apiKey: string): Record<string, string>;
@@ -1,4 +1,9 @@
1
- export const DRUGBANK_BASE = "https://api.drugbank.com/v1";
1
+ /**
2
+ * The Discovery API of DrugBank. The Clinical API (`/v1`) serves no `?q=` search
3
+ * on its drug resource, thus only Discovery answers a name search and a target
4
+ * search.
5
+ */
6
+ export const DRUGBANK_BASE = "https://api.drugbank.com/discovery/v1";
2
7
  export function getDrugbankHeaders(apiKey) {
3
8
  const key = apiKey;
4
9
  if (!key) {
@@ -7,7 +7,12 @@
7
7
  * Article 20, etc.). We fetch and cache it in-process; per-drug queries
8
8
  * filter the cached records by INN, referral name, and associated
9
9
  * medicine names.
10
+ *
11
+ * Absence policy: the bulk file gives each key and writes an absent value as
12
+ * the empty string. Thus a field carries neither `.nullable()` nor
13
+ * `.optional()`.
10
14
  */
15
+ import { z } from "zod";
11
16
  export interface EmaReferral {
12
17
  category: string;
13
18
  referralName: string;
@@ -29,6 +34,54 @@ export interface EmaReferral {
29
34
  lastUpdatedDate: string;
30
35
  referralUrl: string;
31
36
  }
37
+ /** The catalogue file. It is exported so that the golden-fixture table drives it. */
38
+ export declare const ReferralFileSchema: z.ZodObject<{
39
+ meta: z.ZodOptional<z.ZodObject<{
40
+ total_records: z.ZodOptional<z.ZodNumber>;
41
+ timestamp: z.ZodOptional<z.ZodString>;
42
+ }, z.core.$strip>>;
43
+ data: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodObject<{
44
+ category: z.ZodOptional<z.ZodNullable<z.ZodString>>;
45
+ referral_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
46
+ international_non_proprietary_name_inn_common_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
47
+ current_status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
48
+ safety_referral: z.ZodOptional<z.ZodNullable<z.ZodString>>;
49
+ referral_type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
50
+ associated_names_centrally_authorised_medicines: z.ZodOptional<z.ZodNullable<z.ZodString>>;
51
+ associated_names_non_centrally_authorised_medicines: z.ZodOptional<z.ZodNullable<z.ZodString>>;
52
+ class: z.ZodOptional<z.ZodNullable<z.ZodString>>;
53
+ reference_number: z.ZodOptional<z.ZodNullable<z.ZodString>>;
54
+ authorisation_model: z.ZodOptional<z.ZodNullable<z.ZodString>>;
55
+ procedure_start_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
56
+ prac_recommendation_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
57
+ cmdh_position_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
58
+ chmp_cvmp_opinion_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
59
+ european_commission_decision_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
60
+ first_published_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
61
+ last_updated_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
62
+ referral_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
63
+ }, z.core.$strip>, z.ZodTransform<EmaReferral, {
64
+ category?: string | null | undefined;
65
+ referral_name?: string | null | undefined;
66
+ international_non_proprietary_name_inn_common_name?: string | null | undefined;
67
+ current_status?: string | null | undefined;
68
+ safety_referral?: string | null | undefined;
69
+ referral_type?: string | null | undefined;
70
+ associated_names_centrally_authorised_medicines?: string | null | undefined;
71
+ associated_names_non_centrally_authorised_medicines?: string | null | undefined;
72
+ class?: string | null | undefined;
73
+ reference_number?: string | null | undefined;
74
+ authorisation_model?: string | null | undefined;
75
+ procedure_start_date?: string | null | undefined;
76
+ prac_recommendation_date?: string | null | undefined;
77
+ cmdh_position_date?: string | null | undefined;
78
+ chmp_cvmp_opinion_date?: string | null | undefined;
79
+ european_commission_decision_date?: string | null | undefined;
80
+ first_published_date?: string | null | undefined;
81
+ last_updated_date?: string | null | undefined;
82
+ referral_url?: string | null | undefined;
83
+ }>>>>;
84
+ }, z.core.$strip>;
32
85
  /**
33
86
  * Download (or return cached) full EMA referrals catalogue. The catalogue
34
87
  * updates twice daily — a 6h in-process TTL is well within freshness
@@ -7,26 +7,37 @@
7
7
  * Article 20, etc.). We fetch and cache it in-process; per-drug queries
8
8
  * filter the cached records by INN, referral name, and associated
9
9
  * medicine names.
10
+ *
11
+ * Absence policy: the bulk file gives each key and writes an absent value as
12
+ * the empty string. Thus a field carries neither `.nullable()` nor
13
+ * `.optional()`.
10
14
  */
11
15
  import { z } from "zod";
12
16
  import { apiFetchValidated, describeApiError } from "./api-utils.js";
13
17
  import { EMA_REFERRALS_URL } from "./ema-config.js";
18
+ /**
19
+ * Split one medicine column into names.
20
+ *
21
+ * The separator is the semicolon alone. A comma belongs to the name of the
22
+ * medicine, for example `Lidocaïne / Prilocaïne 5% Focus, Crème`, thus a split on
23
+ * the comma breaks that name into two.
24
+ */
14
25
  function parseMedicineList(field) {
15
26
  if (!field || !field.trim())
16
27
  return [];
17
28
  return field
18
- .split(/[;,]/)
29
+ .split(";")
19
30
  .map((s) => s.trim())
20
31
  .filter(Boolean);
21
32
  }
22
- // One schema that both validates the raw referral wire shape (snake_case, every
23
- // field optional or null — the bulk file omits or nulls absent values) and
33
+ // One schema that both validates the raw referral wire shape (snake_case) and
24
34
  // normalizes it into the camelCase `EmaReferral` we return. Parsing IS the
25
35
  // validation: `apiFetchValidated` runs this over the download, so a record whose
26
36
  // field TYPES drift is rejected as `invalid_response` instead of silently
27
- // mis-mapped. Every field is `.nullable()` so one null-bearing record in the
28
- // ~600-row catalogue doesn't fail the whole `data` array (the transform already
29
- // collapses null to "" via `??`).
37
+ // mis-mapped. The sampled records give each key and write an absent value as the
38
+ // empty string. The modifiers stay wider than that sample, thus one odd record in
39
+ // the ~600-row catalogue does not fail the whole `data` array. The transform
40
+ // collapses a null to "" with `??`.
30
41
  const EmaReferralSchema = z
31
42
  .object({
32
43
  category: z.string().nullable().optional(),
@@ -70,7 +81,8 @@ const EmaReferralSchema = z
70
81
  lastUpdatedDate: raw.last_updated_date ?? "",
71
82
  referralUrl: raw.referral_url ?? "",
72
83
  }));
73
- const ReferralFileSchema = z.object({
84
+ /** The catalogue file. It is exported so that the golden-fixture table drives it. */
85
+ export const ReferralFileSchema = z.object({
74
86
  meta: z.object({ total_records: z.number().optional(), timestamp: z.string().optional() }).optional(),
75
87
  data: z.array(EmaReferralSchema).optional(),
76
88
  });
@@ -2,6 +2,10 @@
2
2
  * Pure async client functions for the Ensembl REST API.
3
3
  *
4
4
  * Used by §1.1 (Entity resolution) and as a fallback for ID resolution.
5
+ *
6
+ * Absence policy: Ensembl 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
  export interface GeneInfo {
7
11
  symbol: string;
@@ -2,6 +2,10 @@
2
2
  * Pure async client functions for the Ensembl REST API.
3
3
  *
4
4
  * Used by §1.1 (Entity resolution) and as a fallback for ID resolution.
5
+ *
6
+ * Absence policy: Ensembl 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";
@@ -5,6 +5,10 @@
5
5
  * per-SNP associations), by trait (EFO trait search then its associations), by
6
6
  * variant (direct rsID associations), and by study (the associations of one
7
7
  * GCST study accession).
8
+ *
9
+ * Absence policy: the sampled payloads show an explicit `null` for an absent
10
+ * leaf value, and the key set of one projection stays constant. A key is absent
11
+ * only when the projection that the request named excludes it.
8
12
  */
9
13
  import { z } from "zod";
10
14
  export interface GwasAssociation {
@@ -38,7 +42,9 @@ declare const RawGwasAssociationSchema: z.ZodObject<{
38
42
  }, z.core.$strip>>>;
39
43
  study: z.ZodOptional<z.ZodObject<{
40
44
  accessionId: z.ZodOptional<z.ZodString>;
41
- pubmedId: z.ZodOptional<z.ZodString>;
45
+ publicationInfo: z.ZodOptional<z.ZodObject<{
46
+ pubmedId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
47
+ }, z.core.$strip>>;
42
48
  initialSampleSize: z.ZodOptional<z.ZodString>;
43
49
  }, z.core.$strip>>;
44
50
  pvalueMantissa: z.ZodOptional<z.ZodNumber>;
@@ -49,6 +55,63 @@ declare const RawGwasAssociationSchema: z.ZodObject<{
49
55
  range: z.ZodOptional<z.ZodNullable<z.ZodString>>;
50
56
  }, z.core.$strip>;
51
57
  type RawGwasAssociation = z.infer<typeof RawGwasAssociationSchema>;
58
+ export declare const GwasEmbeddedSchema: z.ZodObject<{
59
+ _embedded: z.ZodOptional<z.ZodObject<{
60
+ associations: z.ZodOptional<z.ZodArray<z.ZodObject<{
61
+ _snpRsId: z.ZodOptional<z.ZodString>;
62
+ loci: z.ZodOptional<z.ZodArray<z.ZodObject<{
63
+ strongestRiskAlleles: z.ZodOptional<z.ZodArray<z.ZodObject<{
64
+ riskAlleleName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
65
+ }, z.core.$strip>>>;
66
+ authorReportedGenes: z.ZodOptional<z.ZodArray<z.ZodObject<{
67
+ geneName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
68
+ }, z.core.$strip>>>;
69
+ }, z.core.$strip>>>;
70
+ efoTraits: z.ZodOptional<z.ZodArray<z.ZodObject<{
71
+ trait: z.ZodOptional<z.ZodNullable<z.ZodString>>;
72
+ }, z.core.$strip>>>;
73
+ study: z.ZodOptional<z.ZodObject<{
74
+ accessionId: z.ZodOptional<z.ZodString>;
75
+ publicationInfo: z.ZodOptional<z.ZodObject<{
76
+ pubmedId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
77
+ }, z.core.$strip>>;
78
+ initialSampleSize: z.ZodOptional<z.ZodString>;
79
+ }, z.core.$strip>>;
80
+ pvalueMantissa: z.ZodOptional<z.ZodNumber>;
81
+ pvalueExponent: z.ZodOptional<z.ZodNumber>;
82
+ riskFrequency: z.ZodOptional<z.ZodNullable<z.ZodString>>;
83
+ orPerCopyNum: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
84
+ betaNum: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
85
+ range: z.ZodOptional<z.ZodNullable<z.ZodString>>;
86
+ }, z.core.$strip>>>;
87
+ }, z.core.$strip>>;
88
+ page: z.ZodOptional<z.ZodObject<{
89
+ totalElements: z.ZodOptional<z.ZodNumber>;
90
+ }, z.core.$strip>>;
91
+ }, z.core.$strip>;
92
+ export declare const GwasTraitSearchResponseSchema: z.ZodObject<{
93
+ _embedded: z.ZodOptional<z.ZodObject<{
94
+ efoTraits: z.ZodOptional<z.ZodArray<z.ZodObject<{
95
+ _links: z.ZodOptional<z.ZodObject<{
96
+ self: z.ZodOptional<z.ZodObject<{
97
+ href: z.ZodOptional<z.ZodString>;
98
+ }, z.core.$strip>>;
99
+ }, z.core.$strip>>;
100
+ }, z.core.$strip>>>;
101
+ }, z.core.$strip>>;
102
+ }, z.core.$strip>;
103
+ export declare const GwasSnpSearchResponseSchema: z.ZodObject<{
104
+ _embedded: z.ZodOptional<z.ZodObject<{
105
+ singleNucleotidePolymorphisms: z.ZodOptional<z.ZodArray<z.ZodObject<{
106
+ rsId: z.ZodOptional<z.ZodString>;
107
+ _links: z.ZodOptional<z.ZodObject<{
108
+ associations: z.ZodOptional<z.ZodObject<{
109
+ href: z.ZodOptional<z.ZodString>;
110
+ }, z.core.$strip>>;
111
+ }, z.core.$strip>>;
112
+ }, z.core.$strip>>>;
113
+ }, z.core.$strip>>;
114
+ }, z.core.$strip>;
52
115
  export declare function mapAssociation(a: RawGwasAssociation): GwasAssociation;
53
116
  export interface GwasSearchOptions {
54
117
  pValueThreshold?: number;
@@ -5,6 +5,10 @@
5
5
  * per-SNP associations), by trait (EFO trait search then its associations), by
6
6
  * variant (direct rsID associations), and by study (the associations of one
7
7
  * GCST study accession).
8
+ *
9
+ * Absence policy: the sampled payloads show an explicit `null` for an absent
10
+ * leaf value, and the key set of one projection stays constant. A key is absent
11
+ * only when the projection that the request named excludes it.
8
12
  */
9
13
  import { z } from "zod";
10
14
  import { apiFetchValidated, describeApiError } from "./api-utils.js";
@@ -12,9 +16,16 @@ import { GWAS_BASE, GWAS_HEADERS } from "./gwas-catalog-config.js";
12
16
  // GWAS Catalog raw wire shapes (HAL+JSON), validated at the fetch boundary.
13
17
  // Every field is optional because the API omits absent values; leaf strings
14
18
  // that the API can return as an explicit `null` are `.nullable()`.
19
+ //
20
+ // The `study` and `efoTraits` keys of an association exist only on some
21
+ // projections: `associationByStudy` nests the study, `associationByEfoTrait`
22
+ // nests the traits, and the gene path serves neither. Thus their absence is a
23
+ // property of the projection that the request named, and it is not drift.
15
24
  const RawGwasStudySchema = z.object({
16
25
  accessionId: z.string().optional(),
17
- pubmedId: z.string().optional(),
26
+ // The PubMed id is a field of the publication record, and never of the study
27
+ // record itself.
28
+ publicationInfo: z.object({ pubmedId: z.string().nullable().optional() }).optional(),
18
29
  initialSampleSize: z.string().optional(),
19
30
  });
20
31
  // `_snpRsId` is not on the wire — the 'gene' search path assigns it after
@@ -36,18 +47,18 @@ const RawGwasAssociationSchema = z.object({
36
47
  betaNum: z.number().nullable().optional(),
37
48
  range: z.string().nullable().optional(),
38
49
  });
39
- const GwasEmbeddedSchema = z.object({
50
+ export const GwasEmbeddedSchema = z.object({
40
51
  _embedded: z.object({ associations: z.array(RawGwasAssociationSchema).optional() }).optional(),
41
52
  page: z.object({ totalElements: z.number().optional() }).optional(),
42
53
  });
43
- const GwasTraitSearchResponseSchema = z.object({
54
+ export const GwasTraitSearchResponseSchema = z.object({
44
55
  _embedded: z
45
56
  .object({
46
57
  efoTraits: z.array(z.object({ _links: z.object({ self: z.object({ href: z.string().optional() }).optional() }).optional() })).optional(),
47
58
  })
48
59
  .optional(),
49
60
  });
50
- const GwasSnpSearchResponseSchema = z.object({
61
+ export const GwasSnpSearchResponseSchema = z.object({
51
62
  _embedded: z
52
63
  .object({
53
64
  singleNucleotidePolymorphisms: z
@@ -79,7 +90,7 @@ export function mapAssociation(a) {
79
90
  trait: traits.join("; "),
80
91
  mappedGenes: [...new Set(genes)],
81
92
  studyAccession: String(study.accessionId ?? ""),
82
- pubmedId: String(study.pubmedId ?? ""),
93
+ pubmedId: String(study.publicationInfo?.pubmedId ?? ""),
83
94
  sampleSize: String(study.initialSampleSize ?? ""),
84
95
  };
85
96
  }
@@ -123,7 +134,11 @@ export async function searchGwasCatalog(query, searchType, options = {}) {
123
134
  associationsUrl = `${GWAS_BASE}/studies/${encodeURIComponent(query)}/associations?projection=associationByStudy&size=${limit}`;
124
135
  }
125
136
  else {
126
- const traitRes = await apiFetchValidated(`${GWAS_BASE}/efoTraits/search/findBySearchQuery?query=${encodeURIComponent(query)}`, GwasTraitSearchResponseSchema, {
137
+ // `findByEfoTrait` matches the EXACT EFO trait label (case-insensitive),
138
+ // and it answers HTTP 200 with an empty `efoTraits` list when nothing
139
+ // matches. A paraphrase of a label thus reads as clean no-data, and the
140
+ // tool description warns the caller.
141
+ const traitRes = await apiFetchValidated(`${GWAS_BASE}/efoTraits/search/findByEfoTrait?trait=${encodeURIComponent(query)}`, GwasTraitSearchResponseSchema, {
127
142
  headers: GWAS_HEADERS,
128
143
  });
129
144
  if (traitRes.isErr())
@@ -17,6 +17,8 @@
17
17
  * resolution failure aborts the assessment with `error.kind:
18
18
  * "target-unresolved"` (per design §6).
19
19
  */
20
+ import { z } from "zod";
21
+ import type { Logger } from "../../lib/logger.js";
20
22
  export interface ResolvedTarget {
21
23
  /** Canonical id used as the entity-resolution anchor — preferred order: HGNC, then Ensembl. */
22
24
  canonicalId: string;
@@ -48,12 +50,26 @@ export interface ResolvedTarget {
48
50
  chembl: boolean;
49
51
  };
50
52
  }
53
+ export declare const HgncResponseSchema: z.ZodObject<{
54
+ response: z.ZodOptional<z.ZodObject<{
55
+ docs: z.ZodOptional<z.ZodArray<z.ZodObject<{
56
+ symbol: z.ZodOptional<z.ZodString>;
57
+ name: z.ZodOptional<z.ZodString>;
58
+ hgnc_id: z.ZodOptional<z.ZodString>;
59
+ ensembl_gene_id: z.ZodOptional<z.ZodString>;
60
+ uniprot_ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
61
+ entrez_id: z.ZodOptional<z.ZodString>;
62
+ alias_symbol: z.ZodOptional<z.ZodArray<z.ZodString>>;
63
+ prev_symbol: z.ZodOptional<z.ZodArray<z.ZodString>>;
64
+ }, z.core.$strip>>>;
65
+ }, z.core.$strip>>;
66
+ }, z.core.$strip>;
51
67
  /**
52
68
  * Resolve a free-form target identifier to a canonical entity.
53
69
  *
54
70
  * @throws when the input cannot be mapped to any human gene/protein.
55
71
  */
56
- export declare function resolveTarget(input: string): Promise<ResolvedTarget>;
72
+ export declare function resolveTarget(input: string, logger?: Logger): Promise<ResolvedTarget>;
57
73
  export interface AutocompleteCandidate {
58
74
  /** Approved HGNC symbol when known, otherwise the input symbol. */
59
75
  symbol: string;
@@ -18,6 +18,7 @@
18
18
  * "target-unresolved"` (per design §6).
19
19
  */
20
20
  import { z } from "zod";
21
+ import { createNoopLogger } from "../../lib/console-logger.js";
21
22
  import { apiFetchValidated } from "./api-utils.js";
22
23
  import { resolveSymbolToEnsemblId } from "./ensembl-client.js";
23
24
  import { searchTargets as searchChemblTargets } from "./chembl-client.js";
@@ -33,7 +34,9 @@ const HGNC_RE = /^HGNC:\d+$/;
33
34
  // HGNC search payload, validated at the fetch boundary. Every field is optional
34
35
  // because the API omits absent values; each read below is optional-chained /
35
36
  // defaulted, so a missing field degrades gracefully rather than throwing.
36
- const HgncResponseSchema = z.object({
37
+ // The schema is exported for the golden-fixture table, which is the only place
38
+ // that asserts the contract itself instead of the resolved entity.
39
+ export const HgncResponseSchema = z.object({
37
40
  response: z
38
41
  .object({
39
42
  docs: z
@@ -69,7 +72,6 @@ const UniprotResponseSchema = z.object({
69
72
  synonyms: z.array(z.object({ value: z.string().optional() })).optional(),
70
73
  }))
71
74
  .optional(),
72
- proteinFamily: z.string().optional(),
73
75
  extraAttributes: z.object({ uniParcId: z.string().optional() }).optional(),
74
76
  keywords: z.array(z.object({ name: z.string().optional() })).optional(),
75
77
  }))
@@ -124,7 +126,8 @@ function extractProteinFamily(uniprotKeywords) {
124
126
  *
125
127
  * @throws when the input cannot be mapped to any human gene/protein.
126
128
  */
127
- export async function resolveTarget(input) {
129
+ export async function resolveTarget(input, logger) {
130
+ const log = (logger ?? createNoopLogger()).named("identifier-resolver").with({ target: input });
128
131
  const trimmed = input.trim();
129
132
  if (!trimmed)
130
133
  throw new Error("Target identifier is required");
@@ -199,7 +202,13 @@ export async function resolveTarget(input) {
199
202
  // Step 4: ChEMBL
200
203
  let chemblId = seedChembl;
201
204
  if (!chemblId && symbolForEnsembl) {
202
- const targets = await searchChemblTargets(symbolForEnsembl, 5).catch(() => []);
205
+ // `searchTargets` answers an unknown symbol with an empty list, thus a throw
206
+ // here is always an unexpected cause. Without this record the ChEMBL id stays
207
+ // null and every downstream collector reports a false "no ChEMBL entry".
208
+ const targets = await searchChemblTargets(symbolForEnsembl, 5).catch((error) => {
209
+ log.error("ChEMBL target search failed", { symbol: symbolForEnsembl, ...log.errorFields(error) });
210
+ return [];
211
+ });
203
212
  const human = targets.find((t) => t.organism === "Homo sapiens");
204
213
  chemblId = (human ?? targets[0])?.targetChemblId ?? null;
205
214
  }
@@ -3,6 +3,10 @@
3
3
  * Consortium) Solr API.
4
4
  *
5
5
  * Used by §3.10.1 (Knockout phenotype card).
6
+ *
7
+ * Absence policy: the Solr API omits the key of an absent value, and it never
8
+ * sends an explicit `null`. Thus a maybe-absent field carries `.optional()`,
9
+ * not `.nullable()`.
6
10
  */
7
11
  export interface MpTerm {
8
12
  id: string;
@@ -3,6 +3,10 @@
3
3
  * Consortium) Solr API.
4
4
  *
5
5
  * Used by §3.10.1 (Knockout phenotype card).
6
+ *
7
+ * Absence policy: the Solr API omits the key of an absent value, and it never
8
+ * sends an explicit `null`. Thus a maybe-absent field carries `.optional()`,
9
+ * not `.nullable()`.
6
10
  */
7
11
  import { z } from "zod";
8
12
  import { apiFetchValidated, describeApiError, isUnexpectedApiError } from "./api-utils.js";
@@ -10,13 +10,14 @@
10
10
  * panels never list a sibling receptor-complex as a developable hit.
11
11
  */
12
12
  import { z } from "zod";
13
- declare const IupharTargetSchema: z.ZodObject<{
13
+ import type { Logger } from "../../lib/logger.js";
14
+ export declare const IupharTargetSchema: z.ZodObject<{
14
15
  targetId: z.ZodNumber;
15
16
  name: z.ZodString;
16
17
  type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
17
18
  familyIds: z.ZodArray<z.ZodNumber>;
18
19
  subunitIds: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
19
- complexIds: z.ZodArray<z.ZodNumber>;
20
+ complexIds: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
20
21
  subunitType: z.ZodOptional<z.ZodString>;
21
22
  }, z.core.$strip>;
22
23
  export type IupharTarget = z.infer<typeof IupharTargetSchema>;
@@ -53,7 +54,7 @@ export declare function getFamilyHeterodimers(uniprotOrGeneSymbol: string): Prom
53
54
  * Example: getAccessoryProteinNames("CALCR") → ["RAMP1", "RAMP2", "RAMP3"].
54
55
  */
55
56
  export declare function getAccessoryProteinNames(uniprotOrGeneSymbol: string): Promise<string[]>;
56
- declare const IupharFamilySchema: z.ZodObject<{
57
+ export declare const IupharFamilySchema: z.ZodObject<{
57
58
  familyId: z.ZodOptional<z.ZodNumber>;
58
59
  name: z.ZodOptional<z.ZodString>;
59
60
  targetIds: z.ZodArray<z.ZodNumber>;
@@ -62,6 +63,11 @@ declare const IupharFamilySchema: z.ZodObject<{
62
63
  }, z.core.$strip>;
63
64
  export type IupharFamily = z.infer<typeof IupharFamilySchema>;
64
65
  export declare function getFamily(familyId: number): Promise<IupharFamily | null>;
66
+ export declare const IupharDatabaseLinkSchema: z.ZodObject<{
67
+ accession: z.ZodOptional<z.ZodString>;
68
+ database: z.ZodOptional<z.ZodString>;
69
+ species: z.ZodOptional<z.ZodString>;
70
+ }, z.core.$strip>;
65
71
  /** Fetch the UniProtKB cross-references (human only) for an IUPHAR target. */
66
72
  export declare function getTargetHumanUniprots(targetId: number): Promise<string[]>;
67
73
  /**
@@ -71,5 +77,4 @@ export declare function getTargetHumanUniprots(targetId: number): Promise<string
71
77
  * CALCRL accession; the AMY1/2/3 complexes are excluded because they are
72
78
  * heterodimers, not separate primary proteins.
73
79
  */
74
- export declare function getFamilySiblingUniprots(uniprotOrGeneSymbol: string): Promise<string[]>;
75
- export {};
80
+ export declare function getFamilySiblingUniprots(uniprotOrGeneSymbol: string, logger?: Logger): Promise<string[]>;