@inflexa-ai/harness 0.4.0 → 0.4.2

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.
@@ -60,9 +60,20 @@ export declare function generateStepSummaryAndWrite(deps: PostStepPipelineDeps,
60
60
  */
61
61
  export declare function reconcileAndRegisterStepArtifacts(deps: PostStepPipelineDeps, postCtx: PostStepContext, manifest: readonly ArtifactManifestEntry[]): Promise<readonly ArtifactManifestEntry[]>;
62
62
  /**
63
- * Vector-index the threaded file descriptions + summary into the per-analysis
64
- * pgvector store. Best-effort (see the artifact-manifest spec): indexing degrades on any failure
65
- * without failing the step — the internal try/catch logs + swallows.
63
+ * Vector-index the threaded file descriptions + the step summary into the
64
+ * per-analysis pgvector store. Best-effort (see the artifact-manifest spec):
65
+ * indexing degrades without failing the step.
66
+ *
67
+ * Degradation is per-item. Index setup (ensuring the index exists and building
68
+ * the store) is all-or-nothing — a setup failure logs and skips the stage,
69
+ * because without an index there is nowhere to index into. Past setup, each
70
+ * file description and the summary is embedded and upserted under its own
71
+ * failure boundary, so one rejected input (e.g. an over-length document the
72
+ * embedding backend rejects) costs only its own entry and every other item is
73
+ * still attempted. A partial index is invisible at the search surface — it
74
+ * returns fewer hits, never an error — so each per-item failure logs the item
75
+ * id and input text length, and when any item fails a final record carries the
76
+ * indexed/failed counts as the only signal that degradation occurred.
66
77
  */
67
78
  export declare function vectorIndexStepOutputs(deps: PostStepPipelineDeps, postCtx: PostStepContext, artifacts: PostStepArtifacts): Promise<void>;
68
79
  /**
@@ -144,60 +144,88 @@ export async function reconcileAndRegisterStepArtifacts(deps, postCtx, manifest)
144
144
  return reconciled.manifest;
145
145
  }
146
146
  /**
147
- * Vector-index the threaded file descriptions + summary into the per-analysis
148
- * pgvector store. Best-effort (see the artifact-manifest spec): indexing degrades on any failure
149
- * without failing the step — the internal try/catch logs + swallows.
147
+ * Vector-index the threaded file descriptions + the step summary into the
148
+ * per-analysis pgvector store. Best-effort (see the artifact-manifest spec):
149
+ * indexing degrades without failing the step.
150
+ *
151
+ * Degradation is per-item. Index setup (ensuring the index exists and building
152
+ * the store) is all-or-nothing — a setup failure logs and skips the stage,
153
+ * because without an index there is nowhere to index into. Past setup, each
154
+ * file description and the summary is embedded and upserted under its own
155
+ * failure boundary, so one rejected input (e.g. an over-length document the
156
+ * embedding backend rejects) costs only its own entry and every other item is
157
+ * still attempted. A partial index is invisible at the search surface — it
158
+ * returns fewer hits, never an error — so each per-item failure logs the item
159
+ * id and input text length, and when any item fails a final record carries the
160
+ * indexed/failed counts as the only signal that degradation occurred.
150
161
  */
151
162
  export async function vectorIndexStepOutputs(deps, postCtx, artifacts) {
152
163
  const { input } = postCtx;
153
164
  const { metadataEntries, summary, reconciledManifest } = artifacts;
165
+ const logger = (deps.logger ?? createNoopLogger()).named("post-step").named("vector-index").with({ runId: input.runId, stepId: input.stepId });
166
+ let vectorStore;
167
+ let indexName;
154
168
  try {
169
+ // Setup is all-or-nothing: without an index there is nothing to index into.
155
170
  await ensureSearchIndex(deps.pool, input.analysisId, deps.embedding.dimensions);
156
- const vectorStore = createVectorStore(deps.pool);
157
- const indexName = searchIndexName(input.analysisId);
158
- const embedOne = async (text) => {
159
- const [vec] = unwrapOrThrow(await deps.embedding.embed([text], postCtx.session));
160
- if (!vec)
161
- throw new Error("vectorIndexStepOutputs: empty embedding response");
162
- return vec;
163
- };
164
- const dbPathPrefix = `runs/${input.runId}/${input.stepId}/`;
165
- const survivingPaths = new Set(reconciledManifest.map((a) => a.path));
166
- const liveMetadata = metadataEntries.filter((e) => survivingPaths.has(e.dbPath.slice(dbPathPrefix.length)));
167
- for (const entry of liveMetadata) {
168
- const embedding = await embedOne(entry.description);
169
- unwrapOrThrow(await vectorStore.upsert({
170
- indexName,
171
- vectors: [embedding],
172
- metadata: [{ text: entry.description, type: "output", ...entry.metadata }],
173
- ids: [`/${input.analysisId}/${entry.dbPath}`],
174
- }));
171
+ vectorStore = createVectorStore(deps.pool);
172
+ indexName = searchIndexName(input.analysisId);
173
+ }
174
+ catch (err) {
175
+ logger.warn("indexing failed", logger.errorFields(err));
176
+ return;
177
+ }
178
+ const embedOne = async (text) => {
179
+ const [vec] = unwrapOrThrow(await deps.embedding.embed([text], postCtx.session));
180
+ if (!vec)
181
+ throw new Error("vectorIndexStepOutputs: empty embedding response");
182
+ return vec;
183
+ };
184
+ /** Index one item, absorbing its failure so the rest of the step still lands. */
185
+ const indexOne = async (id, text, metadata) => {
186
+ try {
187
+ const embedding = await embedOne(text);
188
+ unwrapOrThrow(await vectorStore.upsert({ indexName, vectors: [embedding], metadata: [metadata], ids: [id] }));
189
+ return true;
175
190
  }
176
- if (summary && summary.markdown.trim().length > 0) {
177
- const relPath = `runs/${input.runId}/${input.stepId}/output/summary.md`;
178
- const summaryDbPath = `/${input.analysisId}/${relPath}`;
179
- const summaryEmbedding = await embedOne(summary.markdown);
180
- unwrapOrThrow(await vectorStore.upsert({
181
- indexName,
182
- vectors: [summaryEmbedding],
183
- metadata: [
184
- {
185
- text: summary.markdown,
186
- type: "summary",
187
- stepId: input.stepId,
188
- runId: input.runId,
189
- agentId: input.agentId,
190
- path: relPath,
191
- },
192
- ],
193
- ids: [summaryDbPath],
194
- }));
191
+ catch (err) {
192
+ // Text length rides as a field because over-length input is the known
193
+ // failure driver, and the description/summary itself stays out of logs.
194
+ logger.warn("indexing failed", { id, textLength: text.length, ...logger.errorFields(err) });
195
+ return false;
195
196
  }
197
+ };
198
+ const dbPathPrefix = `runs/${input.runId}/${input.stepId}/`;
199
+ const survivingPaths = new Set(reconciledManifest.map((a) => a.path));
200
+ const liveMetadata = metadataEntries.filter((e) => survivingPaths.has(e.dbPath.slice(dbPathPrefix.length)));
201
+ let indexed = 0;
202
+ let failed = 0;
203
+ for (const entry of liveMetadata) {
204
+ const ok = await indexOne(`/${input.analysisId}/${entry.dbPath}`, entry.description, { text: entry.description, type: "output", ...entry.metadata });
205
+ if (ok)
206
+ indexed++;
207
+ else
208
+ failed++;
196
209
  }
197
- catch (err) {
198
- const logger = (deps.logger ?? createNoopLogger()).named("post-step").named("vector-index");
199
- logger.warn("indexing failed", { runId: input.runId, stepId: input.stepId, ...logger.errorFields(err) });
210
+ if (summary && summary.markdown.trim().length > 0) {
211
+ const relPath = `runs/${input.runId}/${input.stepId}/output/summary.md`;
212
+ const ok = await indexOne(`/${input.analysisId}/${relPath}`, summary.markdown, {
213
+ text: summary.markdown,
214
+ type: "summary",
215
+ stepId: input.stepId,
216
+ runId: input.runId,
217
+ agentId: input.agentId,
218
+ path: relPath,
219
+ });
220
+ if (ok)
221
+ indexed++;
222
+ else
223
+ failed++;
200
224
  }
225
+ // A partial index is invisible at the search surface — it returns fewer hits,
226
+ // never an error — so the count of what did not land is the only signal.
227
+ if (failed > 0)
228
+ logger.warn("indexed with failures", { indexed, failed });
201
229
  }
202
230
  /**
203
231
  * Assemble the step-detail payload (file tree, summary markdown, output file
@@ -17,9 +17,9 @@ export interface ReconcileManifestInput {
17
17
  /** Collector backing the manifest; mutated to reflect drops/rehashes. */
18
18
  collector: ProvenanceCollector;
19
19
  /**
20
- * Operational logging seam; omitted falls back to no-op. Attestation is
21
- * fail-fast, so what this records — which entry was dropped, which input
22
- * could not be hashed — is the account of why a step died.
20
+ * Operational logging seam; omitted falls back to no-op. What this records
21
+ * — which entry or input was dropped, which input could not be hashed — is
22
+ * the sole account of why lineage changed or a step died.
23
23
  */
24
24
  logger?: Logger;
25
25
  }
@@ -23,7 +23,7 @@ import { stat } from "node:fs/promises";
23
23
  import path from "node:path";
24
24
  import { createNoopLogger } from "../lib/console-logger.js";
25
25
  import { computeSha256File, hasValidContentHash } from "../lib/fs-helpers.js";
26
- import { artifactReconcileDropped } from "../lib/metrics.js";
26
+ import { artifactReconcileDropped, lineageInputDropped } from "../lib/metrics.js";
27
27
  export async function reconcileManifestWithDisk(input) {
28
28
  const { workspaceRoot, resourceId, runId, stepId, agentId, manifest, collector } = input;
29
29
  const logger = (input.logger ?? createNoopLogger()).named("reconcile-manifest").with({ runId, stepId, agentId });
@@ -74,7 +74,7 @@ export async function reconcileManifestWithDisk(input) {
74
74
  const hash = await computeSha256File(absPath);
75
75
  reconciled.push({ ...entry, hash, size: info.size });
76
76
  }
77
- await fillInputHashesFromDisk(collector, workspaceRoot, resourceId, runId, stepId, logger);
77
+ await fillInputHashesFromDisk(collector, workspaceRoot, resourceId, runId, stepId, agentId, logger);
78
78
  return { manifest: reconciled, droppedCount };
79
79
  }
80
80
  /**
@@ -87,35 +87,47 @@ export async function reconcileManifestWithDisk(input) {
87
87
  *
88
88
  * `artifacts`-source reads are the step's OWN files (mutating, dropped by the
89
89
  * registration translator), so they are not attested and not hashed here.
90
- * Lineage attestation is fail-fast: an input we cannot hash (gone, or resolving
91
- * outside the analysis tree) throws rather than registering a hashless edge.
90
+ * Lineage attestation is fail-fast for drift: an input that should be on disk
91
+ * and is not (ENOENT) throws rather than registering a hashless edge. A read
92
+ * that resolves OUTSIDE the analysis tree is dropped from lineage instead —
93
+ * the sandbox can legitimately open paths above its mount (`/{resourceId}/..`
94
+ * is the container root) and a capture layer may report them; that is out of
95
+ * scope, not drift, and failing the step over it killed real analyses in the
96
+ * field. Dropping still registers no hashless edge, which is the invariant.
92
97
  */
93
- async function fillInputHashesFromDisk(collector, workspaceRoot, resourceId, runId, stepId, logger) {
98
+ async function fillInputHashesFromDisk(collector, workspaceRoot, resourceId, runId, stepId, agentId, logger) {
94
99
  const resourceRoot = path.resolve(workspaceRoot);
95
100
  for (const ref of collector.getTrackedInputs()) {
96
101
  if (ref.source === "artifacts")
97
102
  continue;
98
103
  if (hasValidContentHash(ref.hash))
99
104
  continue;
100
- // Each throw below is logged before it is raised. The thrown message reaches
101
- // `failStep` as one opaque string and everything downstream of that is
102
- // scrubbed, so naming the site and the offending ref HERE is what makes a
103
- // fail-fast attestation distinguishable from a registry rejection after the
104
- // fact — the read's `source` in particular says whether the step declared
105
+ // Every throw below is logged before it is raised, and every drop is
106
+ // logged as it happens. The thrown message reaches `failStep` as one
107
+ // opaque string and everything downstream of that is scrubbed, so naming
108
+ // the site and the offending ref HERE is the only account of why a step
109
+ // died — the read's `source` in particular says whether the step declared
105
110
  // this input at all.
106
111
  const attestation = { path: ref.path, source: ref.source, refRunId: ref.runId, refStepId: ref.stepId };
107
112
  // `ref.path` is the absolute container path `/{resourceId}/...`; strip the
108
113
  // mount segment and map the tail onto the host workspace root. `path.join`
109
114
  // normalizes any `..`, and the bound confines the read to the analysis tree.
115
+ // An out-of-tree resolution is dropped, not thrown: it is not an attestable
116
+ // lineage edge, but it is also not drift — mirrors the out-of-bounds output
117
+ // skip in `reconcileManifestWithDisk` and the directory drop below.
110
118
  const containerPrefix = `/${resourceId}`;
111
119
  if (ref.path !== containerPrefix && !ref.path.startsWith(containerPrefix + "/")) {
112
- logger.error("input read resolves outside the analysis tree", { ...attestation, throwSite: "container-prefix-bound" });
113
- throw new Error(`[reconcile-manifest] input read resolves outside the analysis tree: path=${ref.path} stepId=${stepId} runId=${runId}`);
120
+ logger.warn("dropping out-of-tree input from lineage", { ...attestation, boundSite: "container-prefix" });
121
+ lineageInputDropped.add(1, { agent_id: agentId, step_id: stepId, reason: "container-prefix" });
122
+ collector.dropInput(ref);
123
+ continue;
114
124
  }
115
125
  const hostPath = path.join(resourceRoot, ref.path.slice(containerPrefix.length + 1));
116
126
  if (hostPath !== resourceRoot && !hostPath.startsWith(resourceRoot + path.sep)) {
117
- logger.error("input read resolves outside the analysis tree", { ...attestation, hostPath, throwSite: "workspace-root-bound" });
118
- throw new Error(`[reconcile-manifest] input read resolves outside the analysis tree: path=${ref.path} stepId=${stepId} runId=${runId}`);
127
+ logger.warn("dropping out-of-tree input from lineage", { ...attestation, hostPath, boundSite: "workspace-root" });
128
+ lineageInputDropped.add(1, { agent_id: agentId, step_id: stepId, reason: "workspace-root" });
129
+ collector.dropInput(ref);
130
+ continue;
119
131
  }
120
132
  let info;
121
133
  try {
@@ -138,6 +150,7 @@ async function fillInputHashesFromDisk(collector, workspaceRoot, resourceId, run
138
150
  // non-file drop above. A genuinely missing FILE (ENOENT) still fails fast
139
151
  // as drift; a directory is not drift.
140
152
  logger.debug("dropping non-file input from lineage", attestation);
153
+ lineageInputDropped.add(1, { agent_id: agentId, step_id: stepId, reason: "directory" });
141
154
  collector.dropInput(ref);
142
155
  continue;
143
156
  }
package/dist/index.d.ts CHANGED
@@ -103,7 +103,7 @@ export { queryStepsByRun } from "./state/step-executions.js";
103
103
  export type { CortexRunRow, StepExecutionRow, RunStatus } from "./state/schema.js";
104
104
  export type { DbError } from "./lib/db-result.js";
105
105
  export { REFERENCE_DATA_CATALOG, REFERENCE_DATA_CATALOG_VERSION, ReferenceArtifactPathSchema, ReferenceArtifactSchema, ReferenceDataCatalogSchema, ReferenceDatasetSchema, ReferenceSha256Schema, UnknownReferenceDatasetError, referenceArtifactKey, resolveReferenceInstallPlan, } from "./reference-data/catalog.js";
106
- export type { ReferenceArtifact, ReferenceDataCatalog, ReferenceDataset, ReferenceInstallPlan, ReferenceInstallPlanDataset, ReferenceIntegrity, } from "./reference-data/catalog.js";
106
+ export type { ReferenceArtifact, ReferenceDataCatalog, ReferenceDataset, ReferenceInstallPlan, ReferenceInstallPlanDataset, } from "./reference-data/catalog.js";
107
107
  export { REFERENCE_INSTALL_RECEIPT_VERSION, ReferenceInstallReceiptSchema, parseReferenceInstallReceipt } from "./reference-data/receipt.js";
108
108
  export type { ReferenceInstallReceipt, ReferenceReceiptArtifact } from "./reference-data/receipt.js";
109
109
  export { queryActiveSandboxes } from "./state/active-sandboxes.js";
@@ -3,5 +3,7 @@
3
3
  *
4
4
  * Instruments:
5
5
  * - cortex.artifact.reconcile.dropped — counter for missing manifest entries
6
+ * - cortex.artifact.reconcile.input_dropped — counter for lineage input drops
6
7
  */
7
8
  export declare const artifactReconcileDropped: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
9
+ export declare const lineageInputDropped: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Instruments:
5
5
  * - cortex.artifact.reconcile.dropped — counter for missing manifest entries
6
+ * - cortex.artifact.reconcile.input_dropped — counter for lineage input drops
6
7
  */
7
8
  import { metrics } from "@opentelemetry/api";
8
9
  const meter = metrics.getMeter("cortex");
@@ -10,3 +11,8 @@ export const artifactReconcileDropped = meter.createCounter("cortex.artifact.rec
10
11
  description: "Manifest entries dropped because their on-disk file was missing at " +
11
12
  "registration time (writes-then-deletes, renames). Tagged by agent_id, step_id.",
12
13
  });
14
+ export const lineageInputDropped = meter.createCounter("cortex.artifact.reconcile.input_dropped", {
15
+ description: "Tracked input reads dropped from lineage at reconcile because they are " +
16
+ "not content-attestable files of the analysis (directory reads, " +
17
+ "out-of-tree resolutions). Tagged by agent_id, step_id, reason.",
18
+ });
@@ -86,7 +86,9 @@ export declare class ProvenanceCollector {
86
86
  * Called by sandbox exec provenance frame processing.
87
87
  *
88
88
  * @param mountPath - The workspace mount path (e.g., "/a1/data" or "/a1" for single-mount)
89
- * @param relativePath - Path relative to the mount root
89
+ * @param relativePath - Path relative to the mount root; or, for a frame
90
+ * report naming somewhere outside the mount, the verbatim absolute path
91
+ * (see `stripMountPrefix`)
90
92
  * @param hash - SHA-256 hash of the file content, or null if file no longer exists
91
93
  * @param context - Explicit classification context from the caller. When provided,
92
94
  * the collector uses it directly instead of classifying from the path.
@@ -124,10 +126,11 @@ export declare class ProvenanceCollector {
124
126
  * Drop an input ref from lineage entirely — removes it from the tracked-input
125
127
  * map and from every record's `inputs` array (identity match, since the refs
126
128
  * are shared objects). `reconcileManifestWithDisk` calls this for a read that
127
- * resolves to a directory (e.g. an `ls` / `list.files` of a mount): the
128
- * inotify frame tracks the open, but a directory is not a content-attestable
129
- * file artifact and must never reach registration. Mirrors the output-side
130
- * non-file drop.
129
+ * is not a content-attestable file of this analysis: one resolving to a
130
+ * directory (e.g. an `ls` / `list.files` of a mount, which the inotify frame
131
+ * tracks as an open), or one resolving outside the analysis tree (e.g.
132
+ * `/{resourceId}/..`, the container root). Neither may reach registration.
133
+ * Mirrors the output-side non-file and out-of-bounds drops.
131
134
  */
132
135
  dropInput(ref: InputRef): void;
133
136
  /**
@@ -121,7 +121,9 @@ export class ProvenanceCollector {
121
121
  * Called by sandbox exec provenance frame processing.
122
122
  *
123
123
  * @param mountPath - The workspace mount path (e.g., "/a1/data" or "/a1" for single-mount)
124
- * @param relativePath - Path relative to the mount root
124
+ * @param relativePath - Path relative to the mount root; or, for a frame
125
+ * report naming somewhere outside the mount, the verbatim absolute path
126
+ * (see `stripMountPrefix`)
125
127
  * @param hash - SHA-256 hash of the file content, or null if file no longer exists
126
128
  * @param context - Explicit classification context from the caller. When provided,
127
129
  * the collector uses it directly instead of classifying from the path.
@@ -133,7 +135,12 @@ export class ProvenanceCollector {
133
135
  return existing;
134
136
  // Use explicit context if provided, otherwise fall back to path classification
135
137
  const classification = context ?? classifyReadPath(relativePath, this.stepId, this.runId, this.dependsOn);
136
- const fullPath = `${mountPath}/${relativePath}`;
138
+ // A still-absolute `relativePath` is a report the caller could not
139
+ // relativize — it names somewhere outside the mount. It rides verbatim:
140
+ // prepending the mount root would forge an in-tree name for a foreign
141
+ // path, and reconcile would then see phantom drift (a missing file,
142
+ // which fails the step) instead of an out-of-tree read (which it drops).
143
+ const fullPath = relativePath.startsWith("/") ? relativePath : `${mountPath}/${relativePath}`;
137
144
  // Resolve fileId for data-source inputs via materialized file metadata map
138
145
  let fileId;
139
146
  if (classification.source === "data" && this.inputFileIdMap) {
@@ -240,10 +247,11 @@ export class ProvenanceCollector {
240
247
  * Drop an input ref from lineage entirely — removes it from the tracked-input
241
248
  * map and from every record's `inputs` array (identity match, since the refs
242
249
  * are shared objects). `reconcileManifestWithDisk` calls this for a read that
243
- * resolves to a directory (e.g. an `ls` / `list.files` of a mount): the
244
- * inotify frame tracks the open, but a directory is not a content-attestable
245
- * file artifact and must never reach registration. Mirrors the output-side
246
- * non-file drop.
250
+ * is not a content-attestable file of this analysis: one resolving to a
251
+ * directory (e.g. an `ls` / `list.files` of a mount, which the inotify frame
252
+ * tracks as an open), or one resolving outside the analysis tree (e.g.
253
+ * `/{resourceId}/..`, the container root). Neither may reach registration.
254
+ * Mirrors the output-side non-file and out-of-bounds drops.
247
255
  */
248
256
  dropInput(ref) {
249
257
  for (const [key, val] of this.inputAccesses) {
@@ -14,7 +14,13 @@
14
14
  */
15
15
  import type { ProvenanceFrame } from "../sandbox/types.js";
16
16
  import { type ProvenanceCollector } from "./collector.js";
17
- /** Strip the `/{resourceId}/` mount prefix from an absolute frame path. */
17
+ /**
18
+ * Strip the `/{resourceId}/` mount prefix from an absolute frame path.
19
+ * Separators doubled at the boundary collapse (`/{rid}//a` → `a`) so an
20
+ * in-mount name lands on its canonical relative form. A path not under the
21
+ * mount comes back unchanged — still absolute — and the collector carries it
22
+ * verbatim (see `trackInputAccess`).
23
+ */
18
24
  export declare function stripMountPrefix(mountRoot: string, absPath: string): string;
19
25
  export interface FeedExecFrameArgs {
20
26
  readonly collector: ProvenanceCollector;
@@ -13,10 +13,16 @@
13
13
  * It never throws — provenance is best-effort and must not fail an exec.
14
14
  */
15
15
  import { classifyReadPath } from "./collector.js";
16
- /** Strip the `/{resourceId}/` mount prefix from an absolute frame path. */
16
+ /**
17
+ * Strip the `/{resourceId}/` mount prefix from an absolute frame path.
18
+ * Separators doubled at the boundary collapse (`/{rid}//a` → `a`) so an
19
+ * in-mount name lands on its canonical relative form. A path not under the
20
+ * mount comes back unchanged — still absolute — and the collector carries it
21
+ * verbatim (see `trackInputAccess`).
22
+ */
17
23
  export function stripMountPrefix(mountRoot, absPath) {
18
24
  const prefix = mountRoot.endsWith("/") ? mountRoot : `${mountRoot}/`;
19
- return absPath.startsWith(prefix) ? absPath.slice(prefix.length) : absPath;
25
+ return absPath.startsWith(prefix) ? absPath.slice(prefix.length).replace(/^\/+/, "") : absPath;
20
26
  }
21
27
  /**
22
28
  * Translate one exec's frame into a collector record. Per-command input
@@ -10,9 +10,12 @@
10
10
  * is required to disambiguate): the gateway emits `402` exclusively for
11
11
  * `budget_exceeded` and `403` for a blocked tenant. Provider-originated
12
12
  * failures (429, 5xx, connection) surface with their own status or as a
13
- * network error with no status at all.
13
+ * network error with no status at all. A `401` is `auth`: the credential the
14
+ * host put behind the call is expired, revoked, or absent, and only a human
15
+ * re-authenticating fixes it — which is a different remedy from every other
16
+ * non-retryable 4xx, so it is a different kind.
14
17
  */
15
- export type ProviderErrorKind = "budget" | "tenant-blocked" | "provider";
18
+ export type ProviderErrorKind = "auth" | "budget" | "tenant-blocked" | "provider";
16
19
  /**
17
20
  * The provider error value channel — a `DomainError`-conforming
18
21
  * discriminated union mirroring `ProviderErrorKind`. `chat` / `embed` return
@@ -23,6 +26,11 @@ export type ProviderErrorKind = "budget" | "tenant-blocked" | "provider";
23
26
  * step boundary.
24
27
  */
25
28
  export type ProviderError = {
29
+ readonly type: "auth";
30
+ readonly retryable: false;
31
+ readonly message: string;
32
+ readonly cause?: unknown;
33
+ } | {
26
34
  readonly type: "budget";
27
35
  readonly retryable: false;
28
36
  readonly message: string;
@@ -72,6 +80,7 @@ export interface ProviderErrorClassification {
72
80
  /**
73
81
  * Classify a provider failure by origin.
74
82
  *
83
+ * - Provider `401` → `auth`, not retryable.
75
84
  * - Billing-gateway `402` → `budget`, not retryable.
76
85
  * - Billing-gateway `403` → `tenant-blocked`, not retryable.
77
86
  * - Provider `429` / `5xx` / connection errors → `provider`, retryable.
@@ -10,7 +10,10 @@
10
10
  * is required to disambiguate): the gateway emits `402` exclusively for
11
11
  * `budget_exceeded` and `403` for a blocked tenant. Provider-originated
12
12
  * failures (429, 5xx, connection) surface with their own status or as a
13
- * network error with no status at all.
13
+ * network error with no status at all. A `401` is `auth`: the credential the
14
+ * host put behind the call is expired, revoked, or absent, and only a human
15
+ * re-authenticating fixes it — which is a different remedy from every other
16
+ * non-retryable 4xx, so it is a different kind.
14
17
  */
15
18
  /**
16
19
  * Structural guard for an already-constructed `ProviderError` value. A
@@ -22,7 +25,9 @@ export function isProviderError(value) {
22
25
  if (typeof value !== "object" || value === null)
23
26
  return false;
24
27
  const v = value;
25
- return (v.type === "budget" || v.type === "tenant-blocked" || v.type === "provider") && typeof v.retryable === "boolean" && typeof v.message === "string";
28
+ return ((v.type === "auth" || v.type === "budget" || v.type === "tenant-blocked" || v.type === "provider") &&
29
+ typeof v.retryable === "boolean" &&
30
+ typeof v.message === "string");
26
31
  }
27
32
  /**
28
33
  * Turn a caught SDK throwable into a `ProviderError` value. Routes through
@@ -42,6 +47,14 @@ export function toProviderError(e, workload) {
42
47
  return e;
43
48
  const { kind, retryable } = classifyProviderError(e);
44
49
  const detail = e instanceof Error ? e.message : String(e);
50
+ if (kind === "auth") {
51
+ return {
52
+ type: "auth",
53
+ retryable: false,
54
+ message: `Provider rejected the credential for ${workload} — it is expired, revoked, or absent: ${detail}`,
55
+ cause: e,
56
+ };
57
+ }
45
58
  if (kind === "budget") {
46
59
  return {
47
60
  type: "budget",
@@ -109,6 +122,7 @@ function looksLikeConnectionError(err) {
109
122
  /**
110
123
  * Classify a provider failure by origin.
111
124
  *
125
+ * - Provider `401` → `auth`, not retryable.
112
126
  * - Billing-gateway `402` → `budget`, not retryable.
113
127
  * - Billing-gateway `403` → `tenant-blocked`, not retryable.
114
128
  * - Provider `429` / `5xx` / connection errors → `provider`, retryable.
@@ -116,6 +130,13 @@ function looksLikeConnectionError(err) {
116
130
  */
117
131
  export function classifyProviderError(e) {
118
132
  const status = extractStatus(e);
133
+ // `auth` is its own kind rather than a plain non-retryable 4xx because the
134
+ // remedy is categorically different: the request was well-formed and the
135
+ // credential behind it is not, so no amount of re-issuing or rephrasing
136
+ // helps — a human has to re-authenticate. Callers surface that; the generic
137
+ // 4xx branch below would tell them the request was wrong, which is false.
138
+ if (status === 401)
139
+ return { kind: "auth", retryable: false };
119
140
  if (status === 402)
120
141
  return { kind: "budget", retryable: false };
121
142
  if (status === 403)
@@ -6,20 +6,20 @@ export declare const REFERENCE_DATA_CATALOG_VERSION: 1;
6
6
  export declare const ReferenceArtifactPathSchema: z.ZodString;
7
7
  /** Lowercase SHA-256 digest of one artifact's bytes. */
8
8
  export declare const ReferenceSha256Schema: z.ZodString;
9
- /** One immutable final file distributed for a reference dataset. */
10
- export declare const ReferenceArtifactSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
11
- integrity: z.ZodLiteral<"pinned">;
12
- path: z.ZodString;
13
- url: z.ZodURL;
14
- bytes: z.ZodNumber;
15
- sha256: z.ZodString;
16
- }, z.core.$strict>, z.ZodObject<{
17
- integrity: z.ZodLiteral<"unpinned">;
9
+ /**
10
+ * One final file distributed for a reference dataset: a stable install-relative
11
+ * path and the third-party https URL it is fetched from. The catalog carries no
12
+ * size or digest — every upstream is authenticated over TLS at download time,
13
+ * and the installer records the bytes it actually received in the receipt so
14
+ * `verify` can later prove the local copy has not changed since install. This is
15
+ * one uniform trust-on-first-use model; there is deliberately no per-artifact
16
+ * integrity class, so adding a source is only ever a URL, and no checked-in
17
+ * digest is ours to maintain or lets go stale when a `current` upstream rebuilds.
18
+ */
19
+ export declare const ReferenceArtifactSchema: z.ZodObject<{
18
20
  path: z.ZodString;
19
21
  url: z.ZodURL;
20
- }, z.core.$strict>], "integrity">;
21
- /** Which integrity guarantee an artifact can actually offer. */
22
- export type ReferenceIntegrity = z.infer<typeof ReferenceArtifactSchema>["integrity"];
22
+ }, z.core.$strict>;
23
23
  /** Provenance and licensing information for one supported reference dataset. */
24
24
  export declare const ReferenceDatasetSchema: z.ZodObject<{
25
25
  id: z.ZodString;
@@ -35,17 +35,10 @@ export declare const ReferenceDatasetSchema: z.ZodObject<{
35
35
  group: z.ZodString;
36
36
  recommended: z.ZodBoolean;
37
37
  }, z.core.$strip>;
38
- artifacts: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
39
- integrity: z.ZodLiteral<"pinned">;
38
+ artifacts: z.ZodArray<z.ZodObject<{
40
39
  path: z.ZodString;
41
40
  url: z.ZodURL;
42
- bytes: z.ZodNumber;
43
- sha256: z.ZodString;
44
- }, z.core.$strict>, z.ZodObject<{
45
- integrity: z.ZodLiteral<"unpinned">;
46
- path: z.ZodString;
47
- url: z.ZodURL;
48
- }, z.core.$strict>], "integrity">>;
41
+ }, z.core.$strict>>;
49
42
  }, z.core.$strip>;
50
43
  /** Versioned canonical catalog consumed identically by every harness embedder. */
51
44
  export declare const ReferenceDataCatalogSchema: z.ZodObject<{
@@ -64,17 +57,10 @@ export declare const ReferenceDataCatalogSchema: z.ZodObject<{
64
57
  group: z.ZodString;
65
58
  recommended: z.ZodBoolean;
66
59
  }, z.core.$strip>;
67
- artifacts: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
68
- integrity: z.ZodLiteral<"pinned">;
69
- path: z.ZodString;
70
- url: z.ZodURL;
71
- bytes: z.ZodNumber;
72
- sha256: z.ZodString;
73
- }, z.core.$strict>, z.ZodObject<{
74
- integrity: z.ZodLiteral<"unpinned">;
60
+ artifacts: z.ZodArray<z.ZodObject<{
75
61
  path: z.ZodString;
76
62
  url: z.ZodURL;
77
- }, z.core.$strict>], "integrity">>;
63
+ }, z.core.$strict>>;
78
64
  }, z.core.$strip>>;
79
65
  }, z.core.$strip>;
80
66
  type DeepReadonly<T> = T extends (...args: never[]) => unknown ? T : T extends readonly (infer Item)[] ? readonly DeepReadonly<Item>[] : T extends object ? {
@@ -97,14 +83,14 @@ export declare function referenceArtifactKey(dataset: {
97
83
  /**
98
84
  * Canonical release catalog. Every artifact is fetched directly from the third
99
85
  * party that publishes it; this project hosts, mirrors, and redistributes
100
- * nothing. Entries are added only with real upstream URLs, provenance, and
101
- * licensing data — plus a size and digest whenever the upstream publishes
102
- * immutable bytes we can pin to.
86
+ * nothing. Entries are added with a real upstream https URL, provenance, and
87
+ * licensing data — and nothing else per file: no size, no digest to compute or
88
+ * keep in sync.
103
89
  *
104
90
  * `version` is the dataset's upstream release identifier. Datasets built on an
105
91
  * upstream that has no immutable release — NCBI regenerates `gene_info` daily,
106
92
  * Reactome overwrites `current` and deletes prior releases — are versioned
107
- * `current` and carry `unpinned` artifacts.
93
+ * `current`; those with an immutable release carry it verbatim.
108
94
  */
109
95
  export declare const REFERENCE_DATA_CATALOG: ReferenceDataCatalog;
110
96
  /** One selected dataset and its host-neutral installation location. */
@@ -22,34 +22,19 @@ export const ReferenceSha256Schema = z.string().regex(SHA256, "Expected a lowerc
22
22
  */
23
23
  const ReferenceArtifactUrlSchema = z.url().refine((value) => value.startsWith("https://"), "Reference artifacts must be fetched over https");
24
24
  /**
25
- * An artifact whose upstream publishes immutable, versioned bytes. The catalog
26
- * carries the size and digest, so a download is verified against *this file*
27
- * before it is ever activated: a mismatch means the bytes are not what we
28
- * reviewed, and the install fails.
25
+ * One final file distributed for a reference dataset: a stable install-relative
26
+ * path and the third-party https URL it is fetched from. The catalog carries no
27
+ * size or digest — every upstream is authenticated over TLS at download time,
28
+ * and the installer records the bytes it actually received in the receipt so
29
+ * `verify` can later prove the local copy has not changed since install. This is
30
+ * one uniform trust-on-first-use model; there is deliberately no per-artifact
31
+ * integrity class, so adding a source is only ever a URL, and no checked-in
32
+ * digest is ours to maintain or lets go stale when a `current` upstream rebuilds.
29
33
  */
30
- const PinnedReferenceArtifactSchema = z.strictObject({
31
- integrity: z.literal("pinned"),
34
+ export const ReferenceArtifactSchema = z.strictObject({
32
35
  path: ReferenceArtifactPathSchema,
33
36
  url: ReferenceArtifactUrlSchema,
34
- bytes: z.number().int().positive(),
35
- sha256: ReferenceSha256Schema,
36
37
  });
37
- /**
38
- * An artifact whose upstream regenerates the same URL in place — NCBI rebuilds
39
- * `gene_info` continuously and Reactome's `current` release is overwritten and
40
- * its predecessors deleted. No checked-in digest can survive that, and pinning
41
- * one would only guarantee a broken download. Integrity is therefore
42
- * trust-on-first-use: the installer records the bytes it actually received in
43
- * the receipt, and `verify` proves the files have not changed *since install*.
44
- * This is a weaker guarantee than `pinned` and is surfaced as such to the user.
45
- */
46
- const UnpinnedReferenceArtifactSchema = z.strictObject({
47
- integrity: z.literal("unpinned"),
48
- path: ReferenceArtifactPathSchema,
49
- url: ReferenceArtifactUrlSchema,
50
- });
51
- /** One immutable final file distributed for a reference dataset. */
52
- export const ReferenceArtifactSchema = z.discriminatedUnion("integrity", [PinnedReferenceArtifactSchema, UnpinnedReferenceArtifactSchema]);
53
38
  /** Provenance and licensing information for one supported reference dataset. */
54
39
  export const ReferenceDatasetSchema = z.object({
55
40
  id: z.string().regex(SAFE_ID, "Expected a stable lowercase dataset id"),
@@ -112,14 +97,14 @@ function deepFreeze(value) {
112
97
  /**
113
98
  * Canonical release catalog. Every artifact is fetched directly from the third
114
99
  * party that publishes it; this project hosts, mirrors, and redistributes
115
- * nothing. Entries are added only with real upstream URLs, provenance, and
116
- * licensing data — plus a size and digest whenever the upstream publishes
117
- * immutable bytes we can pin to.
100
+ * nothing. Entries are added with a real upstream https URL, provenance, and
101
+ * licensing data — and nothing else per file: no size, no digest to compute or
102
+ * keep in sync.
118
103
  *
119
104
  * `version` is the dataset's upstream release identifier. Datasets built on an
120
105
  * upstream that has no immutable release — NCBI regenerates `gene_info` daily,
121
106
  * Reactome overwrites `current` and deletes prior releases — are versioned
122
- * `current` and carry `unpinned` artifacts.
107
+ * `current`; those with an immutable release carry it verbatim.
123
108
  */
124
109
  export const REFERENCE_DATA_CATALOG = deepFreeze(ReferenceDataCatalogSchema.parse({
125
110
  version: REFERENCE_DATA_CATALOG_VERSION,
@@ -137,7 +122,6 @@ export const REFERENCE_DATA_CATALOG = deepFreeze(ReferenceDataCatalogSchema.pars
137
122
  recommendation: { group: "gene-identifiers", recommended: true },
138
123
  artifacts: [
139
124
  {
140
- integrity: "unpinned",
141
125
  path: "Homo_sapiens.gene_info.gz",
142
126
  url: "https://ftp.ncbi.nlm.nih.gov/gene/DATA/GENE_INFO/Mammalia/Homo_sapiens.gene_info.gz",
143
127
  },
@@ -156,7 +140,6 @@ export const REFERENCE_DATA_CATALOG = deepFreeze(ReferenceDataCatalogSchema.pars
156
140
  recommendation: { group: "gene-identifiers", recommended: true },
157
141
  artifacts: [
158
142
  {
159
- integrity: "unpinned",
160
143
  path: "Mus_musculus.gene_info.gz",
161
144
  url: "https://ftp.ncbi.nlm.nih.gov/gene/DATA/GENE_INFO/Mammalia/Mus_musculus.gene_info.gz",
162
145
  },
@@ -175,12 +158,85 @@ export const REFERENCE_DATA_CATALOG = deepFreeze(ReferenceDataCatalogSchema.pars
175
158
  recommendation: { group: "gene-identifiers", recommended: true },
176
159
  artifacts: [
177
160
  {
178
- integrity: "unpinned",
179
161
  path: "Rattus_norvegicus.gene_info.gz",
180
162
  url: "https://ftp.ncbi.nlm.nih.gov/gene/DATA/GENE_INFO/Mammalia/Rattus_norvegicus.gene_info.gz",
181
163
  },
182
164
  ],
183
165
  },
166
+ {
167
+ // All-species monolithic table (~274 MB), not the per-organism split the gene_info entries use:
168
+ // NCBI publishes Entrez↔Ensembl only in this one combined file. Opt-in for that reason.
169
+ id: "ncbi-gene2ensembl",
170
+ version: "current",
171
+ title: "NCBI Entrez-to-Ensembl gene mapping (all species)",
172
+ description: "NCBI Gene's Entrez Gene ID ↔ Ensembl gene/transcript/protein identifier cross-reference for every species (gene2ensembl), tab-separated and gzipped — roughly 274 MB. NCBI rebuilds this file in place, so it is verified against what you downloaded rather than a checked-in digest.",
173
+ sourceUrl: "https://ftp.ncbi.nlm.nih.gov/gene/DATA/",
174
+ license: {
175
+ identifier: "NCBI-Molecular-Data-Usage-Policy",
176
+ url: "https://www.ncbi.nlm.nih.gov/home/about/policies/",
177
+ },
178
+ recommendation: { group: "gene-identifiers", recommended: false },
179
+ artifacts: [{ path: "gene2ensembl.gz", url: "https://ftp.ncbi.nlm.nih.gov/gene/DATA/gene2ensembl.gz" }],
180
+ },
181
+ {
182
+ // ~2.2 GB all-species monolithic table — the largest catalog entry. Opt-in like reactome-mappings.
183
+ id: "ncbi-gene2refseq",
184
+ version: "current",
185
+ title: "NCBI Entrez-to-RefSeq accession mapping (all species, large)",
186
+ description: "NCBI Gene's Entrez Gene ID ↔ RefSeq RNA/protein/genomic accession cross-reference for every species (gene2refseq), tab-separated and gzipped — roughly 2.2 GB. NCBI rebuilds this file in place, so it is verified against what you downloaded rather than a checked-in digest. Only needed to join RefSeq accessions onto genes.",
187
+ sourceUrl: "https://ftp.ncbi.nlm.nih.gov/gene/DATA/",
188
+ license: {
189
+ identifier: "NCBI-Molecular-Data-Usage-Policy",
190
+ url: "https://www.ncbi.nlm.nih.gov/home/about/policies/",
191
+ },
192
+ recommendation: { group: "gene-identifiers", recommended: false },
193
+ artifacts: [{ path: "gene2refseq.gz", url: "https://ftp.ncbi.nlm.nih.gov/gene/DATA/gene2refseq.gz" }],
194
+ },
195
+ {
196
+ id: "uniprot-idmapping-human",
197
+ version: "current",
198
+ title: "UniProt human ID mapping",
199
+ description: "UniProtKB accession ↔ Entrez, Ensembl, RefSeq and other identifiers for Homo sapiens (idmapping_selected, ~61 MB gzipped). UniProt overwrites `current_release` each cycle, so it is verified against what you downloaded rather than a checked-in digest.",
200
+ sourceUrl: "https://ftp.uniprot.org/pub/databases/uniprot/current_release/knowledgebase/idmapping/by_organism/",
201
+ license: { identifier: "CC-BY-4.0", url: "https://www.uniprot.org/help/license" },
202
+ recommendation: { group: "gene-identifiers", recommended: true },
203
+ artifacts: [
204
+ {
205
+ path: "HUMAN_9606_idmapping_selected.tab.gz",
206
+ url: "https://ftp.uniprot.org/pub/databases/uniprot/current_release/knowledgebase/idmapping/by_organism/HUMAN_9606_idmapping_selected.tab.gz",
207
+ },
208
+ ],
209
+ },
210
+ {
211
+ id: "uniprot-idmapping-mouse",
212
+ version: "current",
213
+ title: "UniProt mouse ID mapping",
214
+ description: "UniProtKB accession ↔ Entrez, Ensembl, RefSeq and other identifiers for Mus musculus (idmapping_selected, ~18 MB gzipped). UniProt overwrites `current_release` each cycle, so it is verified against what you downloaded rather than a checked-in digest.",
215
+ sourceUrl: "https://ftp.uniprot.org/pub/databases/uniprot/current_release/knowledgebase/idmapping/by_organism/",
216
+ license: { identifier: "CC-BY-4.0", url: "https://www.uniprot.org/help/license" },
217
+ recommendation: { group: "gene-identifiers", recommended: true },
218
+ artifacts: [
219
+ {
220
+ path: "MOUSE_10090_idmapping_selected.tab.gz",
221
+ url: "https://ftp.uniprot.org/pub/databases/uniprot/current_release/knowledgebase/idmapping/by_organism/MOUSE_10090_idmapping_selected.tab.gz",
222
+ },
223
+ ],
224
+ },
225
+ {
226
+ id: "uniprot-idmapping-rat",
227
+ version: "current",
228
+ title: "UniProt rat ID mapping",
229
+ description: "UniProtKB accession ↔ Entrez, Ensembl, RefSeq and other identifiers for Rattus norvegicus (idmapping_selected, ~6 MB gzipped). UniProt overwrites `current_release` each cycle, so it is verified against what you downloaded rather than a checked-in digest.",
230
+ sourceUrl: "https://ftp.uniprot.org/pub/databases/uniprot/current_release/knowledgebase/idmapping/by_organism/",
231
+ license: { identifier: "CC-BY-4.0", url: "https://www.uniprot.org/help/license" },
232
+ recommendation: { group: "gene-identifiers", recommended: true },
233
+ artifacts: [
234
+ {
235
+ path: "RAT_10116_idmapping_selected.tab.gz",
236
+ url: "https://ftp.uniprot.org/pub/databases/uniprot/current_release/knowledgebase/idmapping/by_organism/RAT_10116_idmapping_selected.tab.gz",
237
+ },
238
+ ],
239
+ },
184
240
  {
185
241
  id: "reactome-pathways",
186
242
  version: "current",
@@ -190,8 +246,8 @@ export const REFERENCE_DATA_CATALOG = deepFreeze(ReferenceDataCatalogSchema.pars
190
246
  license: { identifier: "CC0-1.0", url: "https://reactome.org/license" },
191
247
  recommendation: { group: "pathways", recommended: true },
192
248
  artifacts: [
193
- { integrity: "unpinned", path: "ReactomePathways.gmt", url: "https://reactome.org/download/current/ReactomePathways.gmt" },
194
- { integrity: "unpinned", path: "ReactomePathways.txt", url: "https://reactome.org/download/current/ReactomePathways.txt" },
249
+ { path: "ReactomePathways.gmt", url: "https://reactome.org/download/current/ReactomePathways.gmt" },
250
+ { path: "ReactomePathways.txt", url: "https://reactome.org/download/current/ReactomePathways.txt" },
195
251
  ],
196
252
  },
197
253
  {
@@ -208,17 +264,14 @@ export const REFERENCE_DATA_CATALOG = deepFreeze(ReferenceDataCatalogSchema.pars
208
264
  recommendation: { group: "pathways", recommended: false },
209
265
  artifacts: [
210
266
  {
211
- integrity: "unpinned",
212
267
  path: "NCBI2Reactome_All_Levels.txt",
213
268
  url: "https://reactome.org/download/current/NCBI2Reactome_All_Levels.txt",
214
269
  },
215
270
  {
216
- integrity: "unpinned",
217
271
  path: "Ensembl2Reactome_All_Levels.txt",
218
272
  url: "https://reactome.org/download/current/Ensembl2Reactome_All_Levels.txt",
219
273
  },
220
274
  {
221
- integrity: "unpinned",
222
275
  path: "UniProt2Reactome_All_Levels.txt",
223
276
  url: "https://reactome.org/download/current/UniProt2Reactome_All_Levels.txt",
224
277
  },
@@ -234,11 +287,68 @@ export const REFERENCE_DATA_CATALOG = deepFreeze(ReferenceDataCatalogSchema.pars
234
287
  recommendation: { group: "pathways", recommended: true },
235
288
  artifacts: [
236
289
  {
237
- integrity: "pinned",
238
290
  path: "wikipathways_Homo_sapiens.gmt",
239
291
  url: "https://data.wikipathways.org/20260710/gmt/wikipathways-20260710-gmt-Homo_sapiens.gmt",
240
- bytes: 341_474,
241
- sha256: "79615a079246bb0b07cc3505265b1f75ea6cffec88001ce27f644dd86a39c97d",
292
+ },
293
+ ],
294
+ },
295
+ {
296
+ id: "wikipathways-mouse",
297
+ version: "2026.07.10",
298
+ title: "WikiPathways mouse pathways",
299
+ description: "Community-curated Mus musculus pathway gene sets (GMT) from the immutable 2026-07-10 WikiPathways snapshot.",
300
+ sourceUrl: "https://data.wikipathways.org/20260710/gmt/",
301
+ license: { identifier: "CC0-1.0", url: "https://www.wikipathways.org/about/terms.html" },
302
+ recommendation: { group: "pathways", recommended: true },
303
+ artifacts: [
304
+ {
305
+ path: "wikipathways_Mus_musculus.gmt",
306
+ url: "https://data.wikipathways.org/20260710/gmt/wikipathways-20260710-gmt-Mus_musculus.gmt",
307
+ },
308
+ ],
309
+ },
310
+ {
311
+ id: "wikipathways-rat",
312
+ version: "2026.07.10",
313
+ title: "WikiPathways rat pathways",
314
+ description: "Community-curated Rattus norvegicus pathway gene sets (GMT) from the immutable 2026-07-10 WikiPathways snapshot.",
315
+ sourceUrl: "https://data.wikipathways.org/20260710/gmt/",
316
+ license: { identifier: "CC0-1.0", url: "https://www.wikipathways.org/about/terms.html" },
317
+ recommendation: { group: "pathways", recommended: true },
318
+ artifacts: [
319
+ {
320
+ path: "wikipathways_Rattus_norvegicus.gmt",
321
+ url: "https://data.wikipathways.org/20260710/gmt/wikipathways-20260710-gmt-Rattus_norvegicus.gmt",
322
+ },
323
+ ],
324
+ },
325
+ {
326
+ id: "msigdb-hallmark-human",
327
+ version: "2026.1",
328
+ title: "MSigDB human hallmark gene sets",
329
+ description: "The 50 MSigDB hallmark gene sets for Homo sapiens (symbols GMT) — coherent, well-defined biological states and processes for enrichment. From the immutable MSigDB 2026.1 human release.",
330
+ sourceUrl: "https://data.broadinstitute.org/gsea-msigdb/msigdb/",
331
+ license: { identifier: "MSigDB-License", url: "https://www.gsea-msigdb.org/gsea/msigdb/license.jsp" },
332
+ recommendation: { group: "pathways", recommended: true },
333
+ artifacts: [
334
+ {
335
+ path: "h.all.v2026.1.Hs.symbols.gmt",
336
+ url: "https://data.broadinstitute.org/gsea-msigdb/msigdb/release/2026.1.Hs/h.all.v2026.1.Hs.symbols.gmt",
337
+ },
338
+ ],
339
+ },
340
+ {
341
+ id: "msigdb-hallmark-mouse",
342
+ version: "2026.1",
343
+ title: "MSigDB mouse hallmark gene sets",
344
+ description: "The MSigDB mouse hallmark gene sets for Mus musculus (symbols GMT) — the murine ortholog-mapped hallmark collection for enrichment. From the immutable MSigDB 2026.1 mouse release.",
345
+ sourceUrl: "https://data.broadinstitute.org/gsea-msigdb/msigdb/",
346
+ license: { identifier: "MSigDB-License", url: "https://www.gsea-msigdb.org/gsea/msigdb/license.jsp" },
347
+ recommendation: { group: "pathways", recommended: true },
348
+ artifacts: [
349
+ {
350
+ path: "mh.all.v2026.1.Mm.symbols.gmt",
351
+ url: "https://data.broadinstitute.org/gsea-msigdb/msigdb/release/2026.1.Mm/mh.all.v2026.1.Mm.symbols.gmt",
242
352
  },
243
353
  ],
244
354
  },
@@ -252,11 +362,8 @@ export const REFERENCE_DATA_CATALOG = deepFreeze(ReferenceDataCatalogSchema.pars
252
362
  recommendation: { group: "regulatory-networks", recommended: true },
253
363
  artifacts: [
254
364
  {
255
- integrity: "pinned",
256
365
  path: "CollecTRI_regulons.csv",
257
366
  url: "https://zenodo.org/records/8192729/files/CollecTRI_regulons.csv",
258
- bytes: 4_345_649,
259
- sha256: "4473c9189dd53dacc80297709ad1452dda1086a1cc2185f9a56146c261668701",
260
367
  },
261
368
  ],
262
369
  },
@@ -270,21 +377,25 @@ export const REFERENCE_DATA_CATALOG = deepFreeze(ReferenceDataCatalogSchema.pars
270
377
  recommendation: { group: "normal-expression", recommended: true },
271
378
  artifacts: [
272
379
  {
273
- integrity: "pinned",
274
380
  path: "GTEx_Analysis_2017-06-05_v8_RNASeQCv1.1.9_gene_median_tpm.gct.gz",
275
381
  url: "https://storage.googleapis.com/adult-gtex/bulk-gex/v8/rna-seq/GTEx_Analysis_2017-06-05_v8_RNASeQCv1.1.9_gene_median_tpm.gct.gz",
276
- bytes: 6_952_331,
277
- sha256: "ee7201ff2f280b0de5657d4b08e9a240362d9757efed7f7bd5dba35a5f8617b8",
278
382
  },
279
383
  {
280
- integrity: "pinned",
281
384
  path: "GTEx_Analysis_v8_Annotations_SampleAttributesDS.txt",
282
385
  url: "https://storage.googleapis.com/adult-gtex/annotations/v8/metadata-files/GTEx_Analysis_v8_Annotations_SampleAttributesDS.txt",
283
- bytes: 11_512_258,
284
- sha256: "74f6ab4c34ed2648d708a0ae6e6dff324f6c86ea723ae7d1c37d76f5221148f0",
285
386
  },
286
387
  ],
287
388
  },
389
+ {
390
+ id: "hpa-proteinatlas",
391
+ version: "current",
392
+ title: "Human Protein Atlas",
393
+ description: "The full Human Protein Atlas table (proteinatlas.tsv.zip, ~7 MB): per-gene RNA/protein tissue expression, tissue specificity, subcellular location, secretome and blood-concentration annotations for Homo sapiens. HPA republishes this file per release, so it is verified against what you downloaded rather than a checked-in digest.",
394
+ sourceUrl: "https://www.proteinatlas.org/about/download",
395
+ license: { identifier: "CC-BY-SA-3.0", url: "https://www.proteinatlas.org/about/licence" },
396
+ recommendation: { group: "normal-expression", recommended: true },
397
+ artifacts: [{ path: "proteinatlas.tsv.zip", url: "https://www.proteinatlas.org/download/proteinatlas.tsv.zip" }],
398
+ },
288
399
  {
289
400
  id: "celltypist-immune",
290
401
  version: "2",
@@ -295,18 +406,112 @@ export const REFERENCE_DATA_CATALOG = deepFreeze(ReferenceDataCatalogSchema.pars
295
406
  recommendation: { group: "cell-typing", recommended: true },
296
407
  artifacts: [
297
408
  {
298
- integrity: "pinned",
299
409
  path: "Immune_All_Low.pkl",
300
410
  url: "https://celltypist.cog.sanger.ac.uk/models/Pan_Immune_CellTypist/v2/Immune_All_Low.pkl",
301
- bytes: 2_824_990,
302
- sha256: "290874d35dac039d4c9218c343fde4aac1077709b72a331ce7266f6828c36502",
303
411
  },
304
412
  {
305
- integrity: "pinned",
306
413
  path: "Immune_All_High.pkl",
307
414
  url: "https://celltypist.cog.sanger.ac.uk/models/Pan_Immune_CellTypist/v2/Immune_All_High.pkl",
308
- bytes: 1_070_426,
309
- sha256: "a715fec36c2c421f7c4e31cf4cb4bea883eedab7fd7b20a4b76f091c22660448",
415
+ },
416
+ ],
417
+ },
418
+ {
419
+ id: "celltypist-pan-fetal",
420
+ version: "2",
421
+ title: "CellTypist pan-fetal human model",
422
+ description: "CellTypist Pan_Fetal_Human model — cell-type annotation across human fetal tissues. Load by absolute path; CellTypist's by-name lookup expects its own cache layout, which the reference store deliberately does not impersonate.",
423
+ sourceUrl: "https://www.celltypist.org/models",
424
+ license: { identifier: "NOASSERTION" },
425
+ recommendation: { group: "cell-typing", recommended: true },
426
+ artifacts: [
427
+ {
428
+ path: "Pan_Fetal_Human.pkl",
429
+ url: "https://celltypist.cog.sanger.ac.uk/models/Pan_Fetal_Suo/v2/Pan_Fetal_Human.pkl",
430
+ },
431
+ ],
432
+ },
433
+ {
434
+ id: "celltypist-covid19",
435
+ version: "1",
436
+ title: "CellTypist COVID-19 immune model",
437
+ description: "CellTypist COVID19_Immune_Landscape model — immune cell-type annotation trained on the cross-study COVID-19 immune atlas. Load by absolute path; CellTypist's by-name lookup expects its own cache layout, which the reference store deliberately does not impersonate.",
438
+ sourceUrl: "https://www.celltypist.org/models",
439
+ license: { identifier: "NOASSERTION" },
440
+ recommendation: { group: "cell-typing", recommended: true },
441
+ artifacts: [
442
+ {
443
+ path: "COVID19_Immune_Landscape.pkl",
444
+ url: "https://celltypist.cog.sanger.ac.uk/models/COVID19_Immune_Ren/v1/COVID19_Immune_Landscape.pkl",
445
+ },
446
+ ],
447
+ },
448
+ {
449
+ id: "panglaodb-markers",
450
+ version: "2020.03.27",
451
+ title: "PanglaoDB cell-type markers",
452
+ description: "PanglaoDB cell-type marker panel (tab-separated, gzipped): curated marker genes per cell type across human and mouse, for marker-based annotation. From the immutable 27 March 2020 PanglaoDB marker release.",
453
+ sourceUrl: "https://panglaodb.se/markers.html",
454
+ license: { identifier: "NOASSERTION" },
455
+ recommendation: { group: "cell-typing", recommended: true },
456
+ artifacts: [
457
+ {
458
+ path: "PanglaoDB_markers_27_Mar_2020.tsv.gz",
459
+ url: "https://panglaodb.se/markers/PanglaoDB_markers_27_Mar_2020.tsv.gz",
460
+ },
461
+ ],
462
+ },
463
+ {
464
+ id: "azimuth-pbmc",
465
+ version: "1.0.0",
466
+ title: "Azimuth human PBMC reference",
467
+ description: "Azimuth annotated reference for human peripheral blood mononuclear cells: the Seurat reference object plus its Annoy nearest-neighbour index, for supervised mapping and cell-type label transfer. Immutable Zenodo release 4546839.",
468
+ sourceUrl: "https://zenodo.org/records/4546839",
469
+ license: { identifier: "CC-BY-4.0", url: "https://zenodo.org/records/4546839" },
470
+ recommendation: { group: "cell-typing", recommended: true },
471
+ artifacts: [
472
+ {
473
+ path: "ref.Rds",
474
+ url: "https://zenodo.org/records/4546839/files/ref.Rds",
475
+ },
476
+ {
477
+ path: "idx.annoy",
478
+ url: "https://zenodo.org/records/4546839/files/idx.annoy",
479
+ },
480
+ ],
481
+ },
482
+ {
483
+ // ~443 MB (ref + index), so it is opt-in rather than part of a default setup.
484
+ id: "azimuth-tonsil",
485
+ version: "1.0.0",
486
+ title: "Azimuth human tonsil reference",
487
+ description: "Azimuth annotated reference for human tonsil: the Seurat reference object plus its Annoy nearest-neighbour index, for supervised mapping and cell-type label transfer. Immutable Zenodo release 7032928, roughly 443 MB.",
488
+ sourceUrl: "https://zenodo.org/records/7032928",
489
+ license: { identifier: "CC-BY-4.0", url: "https://zenodo.org/records/7032928" },
490
+ recommendation: { group: "cell-typing", recommended: false },
491
+ artifacts: [
492
+ {
493
+ path: "ref.Rds",
494
+ url: "https://zenodo.org/records/7032928/files/ref.Rds",
495
+ },
496
+ {
497
+ path: "idx.annoy",
498
+ url: "https://zenodo.org/records/7032928/files/idx.annoy",
499
+ },
500
+ ],
501
+ },
502
+ {
503
+ // ~842 MB reference genome bundle, PGx-specific, so it is opt-in rather than a default download.
504
+ id: "pharmcat-grch38-fasta",
505
+ version: "GRCh38.p13",
506
+ title: "PharmCAT GRCh38 reference FASTA",
507
+ description: "The GRCh38.p13 reference genome FASTA that PharmCAT's VCF preprocessor expects (bgzip'd with a .fai/.gzi faidx), bundled as a tar — roughly 842 MB. Immutable Zenodo release 7288118. Needed only for pharmacogenomics (PharmCAT) workflows.",
508
+ sourceUrl: "https://zenodo.org/records/7288118",
509
+ license: { identifier: "CC-BY-4.0", url: "https://zenodo.org/records/7288118" },
510
+ recommendation: { group: "pharmacogenomics", recommended: false },
511
+ artifacts: [
512
+ {
513
+ path: "PharmCAT_GRCh38_reference_fasta.tar",
514
+ url: "https://zenodo.org/records/7288118/files/GRCh38_reference_fasta.tar?download=1",
310
515
  },
311
516
  ],
312
517
  },
@@ -3,19 +3,15 @@ import { z } from "zod";
3
3
  export declare const REFERENCE_INSTALL_RECEIPT_VERSION: 1;
4
4
  /**
5
5
  * What was actually written to disk for one artifact. The size and digest are
6
- * *observed at install*, never copied from the catalog — for a `pinned`
7
- * artifact they equal the catalog's (the install would have failed otherwise),
8
- * and for an `unpinned` one they are the only record of what the mutable
9
- * upstream served, which is what `verify` later checks the files against.
6
+ * *observed at install* — the catalog carries neither, so this receipt is the
7
+ * sole record of the bytes the upstream served, and what `verify` later checks
8
+ * the local files against. A receipt written by an older build may still carry
9
+ * an `integrity` field; it is simply ignored (unknown keys are stripped).
10
10
  */
11
11
  declare const ReferenceReceiptArtifactSchema: z.ZodObject<{
12
12
  path: z.ZodString;
13
13
  bytes: z.ZodNumber;
14
14
  sha256: z.ZodString;
15
- integrity: z.ZodEnum<{
16
- pinned: "pinned";
17
- unpinned: "unpinned";
18
- }>;
19
15
  }, z.core.$strip>;
20
16
  /** Optional metadata describing one activated reference dataset version. */
21
17
  export declare const ReferenceInstallReceiptSchema: z.ZodObject<{
@@ -27,10 +23,6 @@ export declare const ReferenceInstallReceiptSchema: z.ZodObject<{
27
23
  path: z.ZodString;
28
24
  bytes: z.ZodNumber;
29
25
  sha256: z.ZodString;
30
- integrity: z.ZodEnum<{
31
- pinned: "pinned";
32
- unpinned: "unpinned";
33
- }>;
34
26
  }, z.core.$strip>>;
35
27
  }, z.core.$strip>;
36
28
  export type ReferenceInstallReceipt = z.infer<typeof ReferenceInstallReceiptSchema>;
@@ -4,16 +4,15 @@ import { ReferenceArtifactPathSchema, ReferenceDatasetSchema, ReferenceSha256Sch
4
4
  export const REFERENCE_INSTALL_RECEIPT_VERSION = 1;
5
5
  /**
6
6
  * What was actually written to disk for one artifact. The size and digest are
7
- * *observed at install*, never copied from the catalog — for a `pinned`
8
- * artifact they equal the catalog's (the install would have failed otherwise),
9
- * and for an `unpinned` one they are the only record of what the mutable
10
- * upstream served, which is what `verify` later checks the files against.
7
+ * *observed at install* — the catalog carries neither, so this receipt is the
8
+ * sole record of the bytes the upstream served, and what `verify` later checks
9
+ * the local files against. A receipt written by an older build may still carry
10
+ * an `integrity` field; it is simply ignored (unknown keys are stripped).
11
11
  */
12
12
  const ReferenceReceiptArtifactSchema = z.object({
13
13
  path: ReferenceArtifactPathSchema,
14
14
  bytes: z.number().int().nonnegative(),
15
15
  sha256: ReferenceSha256Schema,
16
- integrity: z.enum(["pinned", "unpinned"]),
17
16
  });
18
17
  /** Optional metadata describing one activated reference dataset version. */
19
18
  export const ReferenceInstallReceiptSchema = z
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Host-agnostic bioinformatics agent harness \u2014 hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",