@inflexa-ai/harness 0.48.2 → 0.48.4

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 (85) hide show
  1. package/dist/agents/sandbox/index.js +3 -0
  2. package/dist/agents/sandbox/mdv-preparation-agent.d.ts +9 -0
  3. package/dist/agents/sandbox/mdv-preparation-agent.js +37 -0
  4. package/dist/app/chat-turn-workflow.d.ts +3 -1
  5. package/dist/app/chat-turn-workflow.js +19 -3
  6. package/dist/app/chat-turn.d.ts +4 -2
  7. package/dist/app/chat-turn.js +70 -44
  8. package/dist/app/in-process-turn.js +3 -6
  9. package/dist/app/work.d.ts +7 -3
  10. package/dist/app/work.js +17 -16
  11. package/dist/contracts/chat-events.d.ts +5 -0
  12. package/dist/contracts/chat-parts.d.ts +64 -5
  13. package/dist/contracts/chat-parts.js +3 -1
  14. package/dist/contracts/index.d.ts +1 -1
  15. package/dist/contracts/part-registry.js +2 -0
  16. package/dist/contracts/schemas/chat-events.d.ts +2 -0
  17. package/dist/contracts/schemas/chat-events.js +1 -0
  18. package/dist/contracts/schemas/chat-parts.d.ts +197 -5
  19. package/dist/contracts/schemas/chat-parts.js +32 -2
  20. package/dist/index.d.ts +1 -1
  21. package/dist/lib/regulatory-corpus/fetch-ich.js +1 -16
  22. package/dist/lib/slug.d.ts +18 -0
  23. package/dist/lib/slug.js +29 -0
  24. package/dist/lib/test/pdf-fixture.d.ts +5 -0
  25. package/dist/lib/test/pdf-fixture.js +14 -0
  26. package/dist/loop/chat-stream.js +1 -1
  27. package/dist/loop/run-loop.js +39 -35
  28. package/dist/loop/sub-agent.d.ts +2 -2
  29. package/dist/loop/sub-agent.js +1 -1
  30. package/dist/loop/test/metric-reader.d.ts +9 -0
  31. package/dist/loop/test/metric-reader.js +33 -0
  32. package/dist/loop/test/unnamed-model.d.ts +6 -0
  33. package/dist/loop/test/unnamed-model.js +24 -0
  34. package/dist/memory/conversation-display-recorder.d.ts +2 -0
  35. package/dist/memory/conversation-display-recorder.js +6 -2
  36. package/dist/memory/conversation-display-storage.d.ts +5 -2
  37. package/dist/memory/conversation-display-storage.js +28 -1
  38. package/dist/prompts/conversation.d.ts +1 -1
  39. package/dist/prompts/conversation.js +6 -4
  40. package/dist/prompts/sandbox/mdv-preparation-agent.d.ts +1 -0
  41. package/dist/prompts/sandbox/mdv-preparation-agent.js +56 -0
  42. package/dist/providers/ai-sdk.d.ts +17 -14
  43. package/dist/providers/ai-sdk.js +14 -16
  44. package/dist/report-model/fixture-resolver.js +9 -135
  45. package/dist/report-model/production-resolver.d.ts +3 -3
  46. package/dist/report-model/production-resolver.js +12 -127
  47. package/dist/report-model/reference-read.d.ts +38 -0
  48. package/dist/report-model/reference-read.js +149 -0
  49. package/dist/report-model/reference-resolver.d.ts +7 -0
  50. package/dist/report-model/reference-resolver.js +1 -1
  51. package/dist/report-render/chart.js +48 -46
  52. package/dist/report-render/figures/common.d.ts +34 -0
  53. package/dist/report-render/figures/common.js +4 -1
  54. package/dist/report-render/figures/equal-units.js +2 -4
  55. package/dist/report-render/figures/sankey.js +1 -3
  56. package/dist/report-render/figures/upset.js +2 -5
  57. package/dist/runtime/assemble.js +2 -1
  58. package/dist/sandbox/execution-id.js +2 -14
  59. package/dist/sandbox/leases.js +46 -9
  60. package/dist/schemas/validate-plan.js +2 -14
  61. package/dist/state/active-turns.d.ts +6 -2
  62. package/dist/state/active-turns.js +11 -7
  63. package/dist/state/data-profile.d.ts +8 -0
  64. package/dist/state/data-profile.js +18 -0
  65. package/dist/state/index.d.ts +1 -1
  66. package/dist/state/index.js +1 -1
  67. package/dist/state/migrations/20261102120000_record_the_waiter_of_a_pause.d.ts +6 -0
  68. package/dist/state/migrations/20261102120000_record_the_waiter_of_a_pause.js +10 -0
  69. package/dist/state/migrations/index.js +2 -0
  70. package/dist/state/paused-work.d.ts +5 -3
  71. package/dist/state/paused-work.js +11 -7
  72. package/dist/state/runs.d.ts +3 -2
  73. package/dist/state/runs.js +4 -3
  74. package/dist/tasks/data-profile-index.js +3 -17
  75. package/dist/tasks/data-profile-resolve.js +3 -17
  76. package/dist/tasks/data-profile.js +1 -0
  77. package/dist/tools/lib/opentargets-client.js +15 -25
  78. package/dist/tools/report-authoring/authoring-tools.d.ts +3 -3
  79. package/dist/tools/report-authoring/authoring-tools.js +7 -6
  80. package/dist/tools/research/generate-analogy-report.js +10 -2
  81. package/dist/workflows/execute-analysis.js +4 -3
  82. package/dist/workflows/pause.d.ts +3 -3
  83. package/dist/workflows/pause.js +15 -9
  84. package/dist/workflows/sandbox-step.js +1 -0
  85. package/package.json +7 -4
@@ -0,0 +1,24 @@
1
+ /**
2
+ * One ChatProvider whose stream answers one text delta and one finish that carries the given input-token
3
+ * total and no output tokens. The usage suites feed it to the loop and read the counters back.
4
+ */
5
+ import { convertArrayToReadableStream, MockLanguageModelV4 } from "ai/test";
6
+ import { createAiSdkProvider } from "../../providers/ai-sdk.js";
7
+ export function unnamedModel(inputTokens) {
8
+ const parts = [
9
+ { type: "stream-start", warnings: [] },
10
+ { type: "text-start", id: "p0" },
11
+ { type: "text-delta", id: "p0", delta: "done" },
12
+ { type: "text-end", id: "p0" },
13
+ {
14
+ type: "finish",
15
+ finishReason: { unified: "stop", raw: "stop" },
16
+ usage: {
17
+ inputTokens: { total: inputTokens, noCache: undefined, cacheRead: undefined, cacheWrite: undefined },
18
+ outputTokens: { total: undefined, text: undefined, reasoning: undefined },
19
+ },
20
+ },
21
+ ];
22
+ const model = new MockLanguageModelV4({ provider: "unnamed", doStream: () => Promise.resolve({ stream: convertArrayToReadableStream(parts) }) });
23
+ return createAiSdkProvider({ model, maxRetries: 0 });
24
+ }
@@ -17,6 +17,8 @@ export type ConversationDisplayRecorder = {
17
17
  takeOpening(): ConversationUIMessage[];
18
18
  /** The assistant message of one round, when the recorder got parts after the last take, and the divider of a marker of the round. */
19
19
  takeRound(messages: readonly ModelMessage[]): ConversationUIMessage[];
20
+ /** The `data-pause` parts of the open round, in one assistant message with the id of the round, or none. The round stays open. */
21
+ openRoundPauses(): ConversationUIMessage[];
20
22
  /** The whole turn in one projection. `runChatTurn` stores each round with `takeOpening` and `takeRound` instead. */
21
23
  finish(options?: {
22
24
  readonly fallbackText?: string;
@@ -8,7 +8,7 @@ function jsonCopy(value) {
8
8
  return structuredClone(value);
9
9
  }
10
10
  const DURABLE_CONVERSATION_PART_TYPES = new Set(Object.entries(PART_REGISTRY)
11
- .filter(([, descriptor]) => descriptor.emitter === "conversation" && descriptor.consumer === "conversation" && !descriptor.transient)
11
+ .filter(([, descriptor]) => descriptor.consumer === "conversation" && !descriptor.transient)
12
12
  .map(([type]) => type));
13
13
  function durableConversationType(type) {
14
14
  return DURABLE_CONVERSATION_PART_TYPES.has(type);
@@ -167,6 +167,10 @@ export function createConversationDisplayRecorder(options) {
167
167
  }
168
168
  return taken;
169
169
  }
170
+ function openRoundPauses() {
171
+ const parts = displayParts(roundParts().filter((part) => part.type === "data-pause"));
172
+ return parts.length === 0 ? [] : [{ id: assistantMessageId, role: "assistant", parts: jsonCopy(parts) }];
173
+ }
170
174
  function finish(finishOptions) {
171
175
  if (!finished) {
172
176
  finished = true;
@@ -187,5 +191,5 @@ export function createConversationDisplayRecorder(options) {
187
191
  }
188
192
  return messages;
189
193
  }
190
- return { emit, observe, assistantMessageId: () => assistantMessageId, userMessageId: () => userMessageId, takeOpening, takeRound, finish };
194
+ return { emit, observe, assistantMessageId: () => assistantMessageId, userMessageId: () => userMessageId, takeOpening, takeRound, openRoundPauses, finish };
191
195
  }
@@ -15,7 +15,7 @@
15
15
  * reconstruction.
16
16
  */
17
17
  import { type DataUIPart, type UIMessage } from "ai";
18
- import type { AskPart, ChildSessionStartedPart, CompactionPart, FileReferencePart, PlanApprovalPart, PlanPart, PresentationPart, ReportRenderedPart, RunCardPart, SubAgentActivityEntry, SubAgentActivityPart } from "../contracts/chat-parts.js";
18
+ import type { AnalogyReportPart, AskPart, ChildSessionStartedPart, CompactionPart, FileReferencePart, ModelRefusal, ModelRefusalPart, PausePart, PlanApprovalPart, PlanPart, PresentationPart, ReportRenderedPart, RunCardPart, SubAgentActivityEntry, SubAgentActivityPart } from "../contracts/chat-parts.js";
19
19
  import type { ToolCallOutcome } from "../contracts/chat-events.js";
20
20
  import type { ChatMessage, MessagePart, ToolCallPart } from "../contracts/message.js";
21
21
  import type { Logger } from "../lib/logger.js";
@@ -35,6 +35,7 @@ export type ConversationUIData = {
35
35
  outcome: ToolCallOutcome;
36
36
  };
37
37
  presentation: Payload<PresentationPart>;
38
+ "analogy-report": Payload<AnalogyReportPart>;
38
39
  plan: Payload<PlanPart>;
39
40
  "run-card": Payload<RunCardPart>;
40
41
  "plan-approval": Payload<PlanApprovalPart>;
@@ -43,6 +44,8 @@ export type ConversationUIData = {
43
44
  "child-session-started": Payload<ChildSessionStartedPart>;
44
45
  "report-rendered": Payload<ReportRenderedPart>;
45
46
  compaction: Payload<CompactionPart>;
47
+ "model-refusal": ModelRefusal;
48
+ pause: Payload<PausePart>;
46
49
  "subagent-activity": Omit<Payload<SubAgentActivityPart>, "entries"> & {
47
50
  entries: (Omit<SubAgentActivityEntry, "outcome"> & {
48
51
  outcome: ToolCallOutcome;
@@ -65,7 +68,7 @@ export declare function parseStoredDisplayEnvelope(value: unknown, identity: str
65
68
  export type StoredSourcePart = Extract<Exclude<MessagePart, {
66
69
  type: "text";
67
70
  }>, {
68
- type: ToolCallPart["type"] | PresentationPart["type"] | PlanPart["type"] | RunCardPart["type"] | PlanApprovalPart["type"] | FileReferencePart["type"] | AskPart["type"] | ChildSessionStartedPart["type"] | ReportRenderedPart["type"] | CompactionPart["type"] | SubAgentActivityPart["type"];
71
+ type: ToolCallPart["type"] | PresentationPart["type"] | AnalogyReportPart["type"] | PlanPart["type"] | RunCardPart["type"] | PlanApprovalPart["type"] | FileReferencePart["type"] | AskPart["type"] | ChildSessionStartedPart["type"] | ReportRenderedPart["type"] | CompactionPart["type"] | ModelRefusalPart["type"] | PausePart["type"] | SubAgentActivityPart["type"];
69
72
  }>;
70
73
  /** Whether `part` is an arm the stored vocabulary can hold; anything else cannot become a stored card. */
71
74
  export declare function isStoredSourcePart(part: Exclude<MessagePart, {
@@ -19,7 +19,7 @@ import { textOf } from "../lib/unknown-text.js";
19
19
  import { isDataUIPart, validateUIMessages } from "ai";
20
20
  import { z } from "zod";
21
21
  import { parseUnknownRecord } from "../lib/unknown-containers.js";
22
- import { AskPartSchema, ChildSessionStartedPartSchema, CompactionPartSchema, FileReferencePartSchema, PlanApprovalPartSchema, PlanPartSchema, PresentationPartSchema, ReportRenderedPartSchema, RunCardPartSchema, SubAgentActivityEntrySchema, SubAgentActivityPartSchema, } from "../contracts/schemas/chat-parts.js";
22
+ import { AnalogyReportPartSchema, AskPartSchema, ChildSessionStartedPartSchema, CompactionPartSchema, FileReferencePartSchema, ModelRefusalPayloadSchema, PausePartSchema, PlanApprovalPartSchema, PlanPartSchema, PresentationPartSchema, ReportRenderedPartSchema, RunCardPartSchema, SubAgentActivityEntrySchema, SubAgentActivityPartSchema, } from "../contracts/schemas/chat-parts.js";
23
23
  import { ToolCallOutcomeSchema, ToolCallValueSchema } from "../contracts/schemas/chat-events.js";
24
24
  export const SUPPORTED_DISPLAY_SCHEMA_VERSION = 1;
25
25
  const DisplayEnvelopeHeaderSchema = z.object({
@@ -69,6 +69,7 @@ const ToolCallDisplaySchema = z.object({
69
69
  const dataSchemas = {
70
70
  "tool-call": ToolCallDisplaySchema,
71
71
  presentation: PresentationPartSchema.omit({ type: true }),
72
+ "analogy-report": AnalogyReportPartSchema.omit({ type: true }),
72
73
  plan: PlanPartSchema.omit({ type: true }),
73
74
  "run-card": RunCardPartSchema.omit({ type: true }),
74
75
  "plan-approval": PlanApprovalPartSchema.omit({ type: true }),
@@ -77,6 +78,8 @@ const dataSchemas = {
77
78
  "child-session-started": ChildSessionStartedPartSchema.omit({ type: true }),
78
79
  "report-rendered": ReportRenderedPartSchema.omit({ type: true }),
79
80
  compaction: CompactionPartSchema.omit({ type: true }),
81
+ "model-refusal": ModelRefusalPayloadSchema,
82
+ pause: PausePartSchema.omit({ type: true }),
80
83
  "subagent-activity": SubAgentActivityPartSchema.omit({ type: true }).extend({
81
84
  entries: z.array(SubAgentActivityEntrySchema.extend({ outcome: ToolCallOutcomeSchema })),
82
85
  }),
@@ -174,6 +177,7 @@ export async function parseStoredDisplayEnvelope(value, identity, logger) {
174
177
  const STORED_SOURCE_TYPES = [
175
178
  "tool-call",
176
179
  "data-presentation",
180
+ "data-analogy-report",
177
181
  "data-plan",
178
182
  "data-run-card",
179
183
  "data-plan-approval",
@@ -182,6 +186,8 @@ const STORED_SOURCE_TYPES = [
182
186
  "data-child-session-started",
183
187
  "data-report-rendered",
184
188
  "data-compaction",
189
+ "data-model-refusal",
190
+ "data-pause",
185
191
  "data-subagent-activity",
186
192
  ];
187
193
  const STORED_SOURCE_TYPE_SET = new Set(STORED_SOURCE_TYPES);
@@ -223,6 +229,10 @@ export function conversationDisplayPart(part) {
223
229
  const { type: _type, ...data } = part;
224
230
  return { type: "data-presentation", id, data };
225
231
  }
232
+ case "data-analogy-report": {
233
+ const { type: _type, ...data } = part;
234
+ return { type: "data-analogy-report", id, data };
235
+ }
226
236
  case "data-plan": {
227
237
  const { type: _type, ...data } = part;
228
238
  return { type: "data-plan", id, data };
@@ -255,6 +265,14 @@ export function conversationDisplayPart(part) {
255
265
  const { type: _type, ...data } = part;
256
266
  return { type: "data-compaction", id, data };
257
267
  }
268
+ case "data-model-refusal": {
269
+ const { type: _type, ...data } = part;
270
+ return { type: "data-model-refusal", id, data };
271
+ }
272
+ case "data-pause": {
273
+ const { type: _type, ...data } = part;
274
+ return { type: "data-pause", id, data };
275
+ }
258
276
  case "data-subagent-activity": {
259
277
  const { type: _type, ...payload } = part;
260
278
  return { type: "data-subagent-activity", id, data: { ...payload, entries: part.entries.map(closedEntry) } };
@@ -291,6 +309,9 @@ export function conversationUIToChatMessages(messages) {
291
309
  case "data-presentation":
292
310
  parts.push({ type: "data-presentation", ...part.data });
293
311
  break;
312
+ case "data-analogy-report":
313
+ parts.push({ type: "data-analogy-report", ...part.data });
314
+ break;
294
315
  case "data-plan":
295
316
  parts.push({ type: "data-plan", ...part.data });
296
317
  break;
@@ -315,6 +336,12 @@ export function conversationUIToChatMessages(messages) {
315
336
  case "data-compaction":
316
337
  parts.push({ type: "data-compaction", ...part.data });
317
338
  break;
339
+ case "data-model-refusal":
340
+ parts.push({ type: "data-model-refusal", ...part.data });
341
+ break;
342
+ case "data-pause":
343
+ parts.push({ type: "data-pause", ...part.data });
344
+ break;
318
345
  case "data-subagent-activity":
319
346
  parts.push({ type: "data-subagent-activity", ...part.data });
320
347
  break;
@@ -1 +1 @@
1
- export declare const conversationPrompt = "# Conversation Agent\n\nYou are the user-facing companion of a bioinformatics analysis platform. You\nhelp researchers understand their data, run analyses, interpret results, and\ndevelop testable hypotheses.\n\nYour tool results stay in this conversation across turns. Reuse what you\nalready read or searched, and look again only for something new, for example\na run that completed since.\n\n## Data and Files\n\nYou can read the files of this analysis. `inspect_data_profile` is the record\nof what the dataset is. `workspace_search` finds files by their description,\nand `read_file` gives the values.\n\nWhen the user names data that is not yet an input of this analysis, add it with\nthe input tool if you hold one; its call asks the user. Without one, say that\nthe data must be added as an input. Never tell the user to copy files into a\nfolder to make them visible.\n\n## Analysis Runs\n\nA run starts a sandbox with a specialist agent, and it takes minutes to hours.\nStart one only for computation on the data that needs that environment:\nstatistics, models, R or Python packages, reference data, a table over 16 MiB.\nA lookup, a file read or write, a summary of results that you can read, and\nlight work on a table (a filter, a join, a count, a small test, a reshape, a\nderived file or chart) are not runs. Do them yourself, with `run_script` for\nthe tables.\n\n- **One sandbox computation that the user explicitly asks for** \u2192\n `execute_analysis` in ad hoc mode, with the request in their words. The\n request is the consent: do not plan it and do not ask again. Keep one request\n in one call.\n- **Work of several dependent steps** \u2192 `generate_plan`. It shows the plan\n card. Explain the plan, and ask for approval. On approval, call\n `execute_analysis` in plan mode. A thread record that says that the person\n approved a plan means that its run started already.\n- **A computation that only you suggest** \u2192 ask first.\n\nRuns are asynchronous. Read their results on a later turn with `inspect_run`.\n\n## Interpreting Results\n\nA completed run has one summary for each step. When the run had summaries and\nno blocker, it also has a run synthesis: conclusions, key findings, themes,\nlimitations, and references. `inspect_run` gives the `synthesisStatus` and\nthe `synthesisPath`. Start from the synthesis, and read a step summary for the\nevidence behind a finding. Without a synthesis, integrate the step summaries\nyourself. Group findings by biological theme, and keep well-supported findings\napart from speculation.\n\nFor a translational finding, give the landscape of existing drugs for a target,\nkeep predictive and prognostic biomarkers apart, name the validation that is\nmissing, and rest no claim on one source.\n\n## Literature and Biology\n\nA question about biology or the literature gets a lookup, not an answer from\nmemory. The lookups are cheap API calls in `run_script`: put independent\nlookups in one script, and print only what you need. When a source has nothing,\nsay so and continue with the others.\n\n## Hypotheses\n\nA hypothesis is grounded in data that you read, falsifiable, and specific: the\ngenes, pathways, comparisons, and expected effects. Make sure that the data can\ntest it, rank the candidates by impact, testability, novelty, and evidence, and\ndrop what cannot be tested. Analogies from `generate_analogy_report` are\ncandidate seeds; ground each one in the data before you use it. When the user\nasks to go deeper on a hypothesis, refine it into sharper sub-hypotheses and\ngive the full updated list. To test a hypothesis, plan an analysis for it.\n\nThe analogy report renders as a card. Name the analogies that matter by their\n`analogyTitle`, say why they matter for this data, and end with one question\nabout which one to pursue. Keep analogies out of `generate_plan`,\n`execute_analysis`, and each other workflow input: the planner and the\nsandbox agents do not know analogy reports.\n\n## Reports\n\nWhen the user asks for a report, call `start_report_session` with no\nconfirmation, unless the scope is truly ambiguous: several analyses to choose\nfrom, or an audience that matters and is not known. Then tell the user that the\nreport takes shape in that chat. Do not compose the report here. A request to\nput content into a file, such as a markdown summary or a PDF, is a file that you\nwrite: `write_file` for text, and `export_pdf` in a script for a PDF. The user\nmay not know that report sessions exist: offer one when a structured, cited\nreport would serve them, for example after a run completes.\n\n## Ground Rules\n\n- Cite a value only from a synthesis, a step summary, or a file that you or a\n script read. A `workspace_search` hit gives a description, not the values.\n If you cannot find the data behind a claim, say so.\n- A constraint in the working memory binds the rest of the analysis. If a plan\n or a choice would break one, raise the conflict with the user. Never just\n proceed.\n- Data profiling runs each time the inputs change, thus a plan needs no step\n that explores the data structure.\n- Before you call a package or a reference dataset unavailable, find out\n whether a tool that you hold can provide it.\n- When a tool fails, say so briefly, name a cause that the user can act on, and\n offer a retry or an alternative. Keep the internal details out.\n- When results conflict, give both sides.\n- Give a hypothesis with its ID, claim, rationale, test design, expected\n outcomes, and priority.\n";
1
+ export declare const conversationPrompt = "# Conversation Agent\n\nYou are the user-facing companion of a bioinformatics analysis platform. You\nhelp researchers understand their data, run analyses, interpret results, and\ndevelop testable hypotheses.\n\nYour tool results stay in this conversation across turns. Reuse what you\nalready read or searched, and look again only for something new, for example\na run that completed since.\n\n## Data and Files\n\nYou can read the files of this analysis. `inspect_data_profile` is the record\nof what the dataset is. `workspace_search` finds files by their description,\nand `read_file` gives the values.\n\nWhen the user names data that is not yet an input of this analysis, add it with\nthe input tool if you hold one; its call asks the user. Without one, say that\nthe data must be added as an input. Never tell the user to copy files into a\nfolder to make them visible.\n\n## Analysis Runs\n\nA run starts a sandbox with a specialist agent, and it takes minutes to hours.\nStart one only for computation on the data that needs that environment:\nstatistics, models, R or Python packages, reference data, a table over 16 MiB.\nA lookup, a file read or write, a summary of results that you can read, and\nlight work on a table (a filter, a join, a count, a small test, a reshape, a\nderived file or chart) are not runs. Do them yourself, with `run_script` for\nthe tables.\n\n- **One sandbox computation that the user explicitly asks for** \u2192\n `execute_analysis` in ad hoc mode, with the request in their words. The\n request is the consent: do not plan it and do not ask again. Keep one request\n in one call.\n- **Work of several dependent steps** \u2192 `generate_plan`. It shows the plan\n card. Explain the plan, and ask for approval. On approval, call\n `execute_analysis` in plan mode. A thread record that says that the person\n approved a plan means that its run started already.\n- **A computation that only you suggest** \u2192 ask first.\n\nRuns are asynchronous. Read their results on a later turn with `inspect_run`.\n\n## Interpreting Results\n\nA completed run has one summary for each step. When the run had summaries and\nno blocker, it also has a run synthesis: conclusions, key findings, themes,\nlimitations, and references. `inspect_run` gives the `synthesisStatus` and\nthe `synthesisPath`. Start from the synthesis, and read a step summary for the\nevidence behind a finding. Without a synthesis, integrate the step summaries\nyourself. Group findings by biological theme, and keep well-supported findings\napart from speculation.\n\nFor a translational finding, give the landscape of existing drugs for a target,\nkeep predictive and prognostic biomarkers apart, name the validation that is\nmissing, and rest no claim on one source.\n\n## Literature and Biology\n\nA question about biology or the literature gets a lookup, not an answer from\nmemory. The lookups are cheap API calls in `run_script`: put independent\nlookups in one script, and print only what you need. When a source has nothing,\nsay so and continue with the others.\n\n## Hypotheses\n\nA hypothesis is grounded in data that you read, falsifiable, and specific: the\ngenes, pathways, comparisons, and expected effects. Make sure that the data can\ntest it, rank the candidates by impact, testability, novelty, and evidence, and\ndrop what cannot be tested. Analogies from `generate_analogy_report` are\ncandidate seeds; ground each one in the data before you use it. When the user\nasks to go deeper on a hypothesis, refine it into sharper sub-hypotheses and\ngive the full updated list. To test a hypothesis, plan an analysis for it.\n\nThe analogy block shows the full report to the user, thus do not copy the\nreport into your reply. Name the analogies that matter by their\n`analogyTitle`, say why they matter for this data, and end with one question\nabout which one to pursue. For an `extraction-failed` envelope, give\nthe message of the error and ask the user to narrow the problem. Keep\nanalogies out of `generate_plan`, `execute_analysis`, and each other workflow\ninput: the planner and the sandbox agents do not know analogy reports.\n\n## Reports\n\nWhen the user asks for a report, call `start_report_session` with no\nconfirmation, unless the scope is truly ambiguous: several analyses to choose\nfrom, or an audience that matters and is not known. Then tell the user that the\nreport takes shape in that chat. Do not compose the report here. A request to\nput content into a file, such as a markdown summary or a PDF, is a file that you\nwrite: `write_file` for text, and `export_pdf` in a script for a PDF. The user\nmay not know that report sessions exist: offer one when a structured, cited\nreport would serve them, for example after a run completes.\n\n## Ground Rules\n\n- Cite a value only from a synthesis, a step summary, or a file that you or a\n script read. A `workspace_search` hit gives a description, not the values.\n If you cannot find the data behind a claim, say so.\n- A constraint in the working memory binds the rest of the analysis. If a plan\n or a choice would break one, raise the conflict with the user. Never just\n proceed.\n- Data profiling runs each time the inputs change, thus a plan needs no step\n that explores the data structure.\n- Before you call a package or a reference dataset unavailable, find out\n whether a tool that you hold can provide it.\n- When a tool fails, say so briefly, name a cause that the user can act on, and\n offer a retry or an alternative. Keep the internal details out.\n- When results conflict, give both sides.\n- Give a hypothesis with its ID, claim, rationale, test design, expected\n outcomes, and priority.\n";
@@ -72,11 +72,13 @@ candidate seeds; ground each one in the data before you use it. When the user
72
72
  asks to go deeper on a hypothesis, refine it into sharper sub-hypotheses and
73
73
  give the full updated list. To test a hypothesis, plan an analysis for it.
74
74
 
75
- The analogy report renders as a card. Name the analogies that matter by their
75
+ The analogy block shows the full report to the user, thus do not copy the
76
+ report into your reply. Name the analogies that matter by their
76
77
  \`analogyTitle\`, say why they matter for this data, and end with one question
77
- about which one to pursue. Keep analogies out of \`generate_plan\`,
78
- \`execute_analysis\`, and each other workflow input: the planner and the
79
- sandbox agents do not know analogy reports.
78
+ about which one to pursue. For an \`extraction-failed\` envelope, give
79
+ the message of the error and ask the user to narrow the problem. Keep
80
+ analogies out of \`generate_plan\`, \`execute_analysis\`, and each other workflow
81
+ input: the planner and the sandbox agents do not know analogy reports.
80
82
 
81
83
  ## Reports
82
84
 
@@ -0,0 +1 @@
1
+ export declare const mdvPreparationAgentPrompt = "# MDV Preparation Agent\n\nYou prepare biological datasets as assets for MDV (Multi-Dimensional Viewer). You take\nscRNA-seq, snRNA-seq, scATAC-seq, multiome, spatial transcriptomics (CosMx, Xenium, Visium,\nMERFISH) and spatial proteomics (CODEX, IMC, MIBI) data, in raw or processed form. You deliver an\nMDV-ready AnnData that MDV's upload converts into readable tables, the context tables, and a\nREADME, so that a person who did not make them can use them in MDV.\n\n## Skills\n\nYour skills: `mdv-preparation`, `single-cell`, `spatial-omics`, `chromatin-regulation`,\n`shared/omics-general`.\n\nRead `mdv-preparation` first. Its workflow is the order of your work, and its references\nhold the code to copy: the asset helpers and the validator. Use the modality packs for QC,\nnormalisation, embedding, and clustering when the input is raw.\n\n## Workflow (Summary)\n\n1. **Inventory** \u2014 identify the modality, the platform, the input form (raw instrument output,\n count matrix, processed AnnData, MuData, SpatialData), and the size. Estimate the memory\n before you load a large matrix.\n2. **Build** \u2014 one analysis-ready AnnData: counts in, QC, log-normalised sparse X, UMAP,\n clusters, and readable labels with their evidence.\n3. **Curate** \u2014 keep the columns a person needs, label each with words and units (the label is\n the column name in MDV), keep grouping columns at 256 categories or fewer.\n4. **Write** \u2014 the asset, the context tables, and the README with a column glossary, the\n processing record, and the first charts to make.\n5. **Validate** \u2014 run the validator of the pack. Zero errors before delivery.\n\n## Domain Standards\n\n- The deliverable is an asset, not an MDV project. You do not need mdvtools.\n- Spatial data: slide coordinates in micrometres as two columns, y flipped upwards. For raw CosMx\n runs, measure the FOV orientation before you trust the layout.\n- Inferred labels are putative: say so in the column label and keep the evidence.\n- Fix every seed. The same input gives the same asset.\n\n## Figures\n\nThe asset is the deliverable. Write one preview PNG only if the step needs a figure for the\nreport: the tissue or the UMAP coloured by cell type.\n\n## Domain Anti-Patterns\n\n- Raw snake_case keys or bare cluster ids as the labels people see.\n- Dense or scaled matrices, extra layers, or PCA columns in the asset.\n- Grouping columns above 256 categories.\n- Skipping the validator because the file \"loads\".\n\n## Required Output Files\n\n- The asset `output/*.mdv.h5ad`, its context tables `output/*.tsv`, and `output/README.md`.\n- The validator report `output/*.mdv.validation.json`, with zero errors.\n- The analysis AnnData `output/*.h5ad` with standard keys and raw counts.\n";
@@ -0,0 +1,56 @@
1
+ export const mdvPreparationAgentPrompt = `# MDV Preparation Agent
2
+
3
+ You prepare biological datasets as assets for MDV (Multi-Dimensional Viewer). You take
4
+ scRNA-seq, snRNA-seq, scATAC-seq, multiome, spatial transcriptomics (CosMx, Xenium, Visium,
5
+ MERFISH) and spatial proteomics (CODEX, IMC, MIBI) data, in raw or processed form. You deliver an
6
+ MDV-ready AnnData that MDV's upload converts into readable tables, the context tables, and a
7
+ README, so that a person who did not make them can use them in MDV.
8
+
9
+ ## Skills
10
+
11
+ Your skills: \`mdv-preparation\`, \`single-cell\`, \`spatial-omics\`, \`chromatin-regulation\`,
12
+ \`shared/omics-general\`.
13
+
14
+ Read \`mdv-preparation\` first. Its workflow is the order of your work, and its references
15
+ hold the code to copy: the asset helpers and the validator. Use the modality packs for QC,
16
+ normalisation, embedding, and clustering when the input is raw.
17
+
18
+ ## Workflow (Summary)
19
+
20
+ 1. **Inventory** — identify the modality, the platform, the input form (raw instrument output,
21
+ count matrix, processed AnnData, MuData, SpatialData), and the size. Estimate the memory
22
+ before you load a large matrix.
23
+ 2. **Build** — one analysis-ready AnnData: counts in, QC, log-normalised sparse X, UMAP,
24
+ clusters, and readable labels with their evidence.
25
+ 3. **Curate** — keep the columns a person needs, label each with words and units (the label is
26
+ the column name in MDV), keep grouping columns at 256 categories or fewer.
27
+ 4. **Write** — the asset, the context tables, and the README with a column glossary, the
28
+ processing record, and the first charts to make.
29
+ 5. **Validate** — run the validator of the pack. Zero errors before delivery.
30
+
31
+ ## Domain Standards
32
+
33
+ - The deliverable is an asset, not an MDV project. You do not need mdvtools.
34
+ - Spatial data: slide coordinates in micrometres as two columns, y flipped upwards. For raw CosMx
35
+ runs, measure the FOV orientation before you trust the layout.
36
+ - Inferred labels are putative: say so in the column label and keep the evidence.
37
+ - Fix every seed. The same input gives the same asset.
38
+
39
+ ## Figures
40
+
41
+ The asset is the deliverable. Write one preview PNG only if the step needs a figure for the
42
+ report: the tissue or the UMAP coloured by cell type.
43
+
44
+ ## Domain Anti-Patterns
45
+
46
+ - Raw snake_case keys or bare cluster ids as the labels people see.
47
+ - Dense or scaled matrices, extra layers, or PCA columns in the asset.
48
+ - Grouping columns above 256 categories.
49
+ - Skipping the validator because the file "loads".
50
+
51
+ ## Required Output Files
52
+
53
+ - The asset \`output/*.mdv.h5ad\`, its context tables \`output/*.tsv\`, and \`output/README.md\`.
54
+ - The validator report \`output/*.mdv.validation.json\`, with zero errors.
55
+ - The analysis AnnData \`output/*.h5ad\` with standard keys and raw counts.
56
+ `;
@@ -118,6 +118,12 @@ export type AiSdkProviderConfig = {
118
118
  * Needs the `thinking-binding-controls-2026-08-01` beta header.
119
119
  */
120
120
  readonly thinkingBinding?: "drop_block" | "error" | "off";
121
+ /**
122
+ * `default`: when a safety classifier declines a request, the API sends it again to the model that it
123
+ * recommends for the category of the refusal, inside the same call. When the field is absent, a declined
124
+ * request ends with the content filter. A model with no fallback target keeps its refusal.
125
+ */
126
+ readonly fallbacks?: "default";
121
127
  } | {
122
128
  readonly kind: "openai";
123
129
  readonly baseURL?: string;
@@ -252,22 +258,19 @@ export declare function sessionKeyDigestOf(session: Pick<AgentSession, "scope" |
252
258
  * A model can refuse a request by category — a biology prompt is one such category —
253
259
  * and the API then re-serves it on another model. It reports the hop TWICE: as a
254
260
  * `fallback` content block inside the assistant turn, and on
255
- * `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps only the second,
256
- * because the AI SDK has no part for a model hop, and it drops the block from the
257
- * content it returns (`anthropic-messages-language-model`, the `content_block_start`
258
- * case). Thus the turn the harness holds is not the turn the API produced, and every
259
- * signed thinking block of that turn is rejected from the next request on.
261
+ * `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps the block as a
262
+ * custom part and sends it back on the next request, but it keeps each thinking block
263
+ * before it too. The API accepts no thinking block from before the hop, and a turn
264
+ * whose thinking blocks changed earns `thinking ... blocks in the latest assistant
265
+ * message cannot be modified`, which is a 400 that no retry can pass.
260
266
  *
261
- * The API holds the whole turn against the signature of each of its thinking blocks, so
262
- * echoing that turn earns `thinking ... blocks in the latest assistant message cannot be
263
- * modified`, which is a 400 that no retry can pass. A thinking block is optional on the
264
- * way back, thus the loop records the reply of a hop with no thinking block, and the next
265
- * request continues. The cost is the reasoning of this one turn and the cache write that
266
- * its new prefix pays for.
267
+ * A thinking block is optional on the way back, thus the loop records the reply of a
268
+ * hop with no thinking block, and the next request continues. The cost is the
269
+ * reasoning of this one turn and the cache write that its new prefix pays for.
267
270
  *
268
- * The response metadata of the package carries `message_start.message.model`, and that
269
- * field keeps the REQUESTED model across a hop. The iteration names the model that
270
- * answered, thus it wins for the usage record.
271
+ * The response metadata of the package carries `message_start.message.model`, and
272
+ * after a hop in the middle of a reply that field keeps the REQUESTED model. The
273
+ * iteration names the model that answered, thus it wins for the usage record.
271
274
  */
272
275
  export declare function fallbackModelOf(metadata: ProviderMetadata | undefined): string | undefined;
273
276
  /**
@@ -108,22 +108,19 @@ function thinkingWithBinding(reasoning, mode) {
108
108
  * A model can refuse a request by category — a biology prompt is one such category —
109
109
  * and the API then re-serves it on another model. It reports the hop TWICE: as a
110
110
  * `fallback` content block inside the assistant turn, and on
111
- * `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps only the second,
112
- * because the AI SDK has no part for a model hop, and it drops the block from the
113
- * content it returns (`anthropic-messages-language-model`, the `content_block_start`
114
- * case). Thus the turn the harness holds is not the turn the API produced, and every
115
- * signed thinking block of that turn is rejected from the next request on.
111
+ * `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps the block as a
112
+ * custom part and sends it back on the next request, but it keeps each thinking block
113
+ * before it too. The API accepts no thinking block from before the hop, and a turn
114
+ * whose thinking blocks changed earns `thinking ... blocks in the latest assistant
115
+ * message cannot be modified`, which is a 400 that no retry can pass.
116
116
  *
117
- * The API holds the whole turn against the signature of each of its thinking blocks, so
118
- * echoing that turn earns `thinking ... blocks in the latest assistant message cannot be
119
- * modified`, which is a 400 that no retry can pass. A thinking block is optional on the
120
- * way back, thus the loop records the reply of a hop with no thinking block, and the next
121
- * request continues. The cost is the reasoning of this one turn and the cache write that
122
- * its new prefix pays for.
117
+ * A thinking block is optional on the way back, thus the loop records the reply of a
118
+ * hop with no thinking block, and the next request continues. The cost is the
119
+ * reasoning of this one turn and the cache write that its new prefix pays for.
123
120
  *
124
- * The response metadata of the package carries `message_start.message.model`, and that
125
- * field keeps the REQUESTED model across a hop. The iteration names the model that
126
- * answered, thus it wins for the usage record.
121
+ * The response metadata of the package carries `message_start.message.model`, and
122
+ * after a hop in the middle of a reply that field keeps the REQUESTED model. The
123
+ * iteration names the model that answered, thus it wins for the usage record.
127
124
  */
128
125
  export function fallbackModelOf(metadata) {
129
126
  const entries = metadata?.["anthropic"]?.["iterations"];
@@ -717,12 +714,13 @@ export function createConfiguredAiSdkProvider(deps) {
717
714
  maxRetries: config.maxRetries,
718
715
  requestTimeoutMs,
719
716
  reasoning: config.reasoning,
720
- // The package sends the key as `metadata.user_id`, and the binding as
721
- // `thinking.block_binding`.
717
+ // The package sends the key as `metadata.user_id`, the binding as
718
+ // `thinking.block_binding`, and the default fallback with its beta header.
722
719
  providerOptionsFor: ({ session, reasoning }) => ({
723
720
  anthropic: {
724
721
  metadata: { userId: sessionKeyDigestOf(session) },
725
722
  thinking: binding === undefined ? undefined : thinkingWithBinding(reasoning, binding),
723
+ fallbacks: config.fallbacks,
726
724
  },
727
725
  }),
728
726
  });
@@ -4,17 +4,9 @@
4
4
  * assertion rules without a host.
5
5
  */
6
6
  import { err, ok } from "neverthrow";
7
- import { asFiniteNumber, cellMatchesFilterValue, checkCitationAssertion, checkValueAssertion } from "./assert-rules.js";
8
- import { applyRowBound, columnsHeldByNoRow, fileTypeHoldsNoCell, snapshotEntry, snapshotRows, } from "./reference-resolver.js";
9
- /** Build an `Err` that carries the unresolved reference. The `detail` key is present only when there is a detail to carry. */
10
- function fail(reference, reason, detail) {
11
- const failure = detail !== undefined ? { reference, reason, detail } : { reference, reason };
12
- return err(failure);
13
- }
14
- /** A short account of a derivation input, for a detail that names which input broke the arithmetic. */
15
- function describeReference(reference) {
16
- return `artifact value at ${reference.path} column ${reference.locator.column}`;
17
- }
7
+ import { checkValueAssertion } from "./assert-rules.js";
8
+ import { fail, readArtifactTable, resolveCitation, resolveDerivation, selectRow } from "./reference-read.js";
9
+ import { fileTypeHoldsNoCell, snapshotEntry, snapshotRows } from "./reference-resolver.js";
18
10
  function resolveArtifactValue(reference, snapshot) {
19
11
  const artifact = snapshotEntry(snapshot, reference.path);
20
12
  if (artifact === undefined) {
@@ -35,53 +27,22 @@ function resolveArtifactValue(reference, snapshot) {
35
27
  if (readRows.isErr()) {
36
28
  return fail(reference, "unreadable-artifact", `the artifact at ${reference.path} holds a malformed row: ${readRows.error}`);
37
29
  }
38
- const rows = readRows.value;
39
30
  const locator = reference.locator;
40
- let selectedRow;
41
- if (locator.row !== undefined) {
42
- const row = rows[locator.row];
43
- if (row === undefined) {
44
- return fail(reference, "locator-out-of-range", `row ${locator.row} is outside the ${rows.length} rows`);
45
- }
46
- selectedRow = row;
47
- }
48
- else if (locator.rowFilter !== undefined) {
49
- const filter = locator.rowFilter;
50
- const matches = rows.filter((row) => cellMatchesFilterValue(row[filter.column], filter.value));
51
- const match = matches[0];
52
- if (match === undefined) {
53
- return fail(reference, "locator-out-of-range", `no row where ${filter.column} equals ${String(filter.value)}`);
54
- }
55
- if (matches.length > 1) {
56
- return fail(reference, "ambiguous-match", `${matches.length} rows where ${filter.column} equals ${String(filter.value)}`);
57
- }
58
- selectedRow = match;
59
- }
60
- else {
61
- // The locator schema forbids a locator with neither selector. The resolver cannot depend on a
62
- // prior parse, thus it treats a row that no selector addresses as out of range.
63
- return fail(reference, "locator-out-of-range", "the locator selects no row");
31
+ const selection = selectRow(readRows.value, locator);
32
+ if (selection.isErr()) {
33
+ return fail(reference, selection.error.reason, selection.error.detail);
64
34
  }
65
35
  // A real table holds an empty cell, and the snapshot parser gives it back as an absent
66
36
  // key: `snapshotRows` drops a cell that parsed as `null` or `undefined`, so an absent key
67
37
  // is the only absence. It is not a value that a scalar reference can bind. An empty
68
38
  // string is a value, thus it stays valid.
69
- const cell = selectedRow[locator.column];
39
+ const cell = selection.value[locator.column];
70
40
  if (cell === undefined) {
71
41
  return fail(reference, "locator-out-of-range", `column ${locator.column} holds no value in the selected row`);
72
42
  }
73
43
  const valueFailure = checkValueAssertion(reference, reference.assert?.value, reference.assert?.tolerance, cell);
74
44
  return valueFailure !== undefined ? err(valueFailure) : ok({ type: "scalar", value: cell });
75
45
  }
76
- /**
77
- * Put one own data property on a projected row.
78
- *
79
- * A plain assignment at the key `__proto__` reaches the prototype setter, thus the projection loses a
80
- * column that the row holds. The definition makes an own data property for each key.
81
- */
82
- function defineOwn(target, key, value) {
83
- Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });
84
- }
85
46
  function resolveArtifactTable(reference, snapshot) {
86
47
  const artifact = snapshotEntry(snapshot, reference.path);
87
48
  if (artifact === undefined) {
@@ -94,46 +55,11 @@ function resolveArtifactTable(reference, snapshot) {
94
55
  if (fileTypeHoldsNoCell(tableFileType)) {
95
56
  return fail(reference, "unreadable-artifact", `the ${String(tableFileType)} at ${reference.path} holds no cell to read`);
96
57
  }
97
- // The bound ranks the rows as the artifact holds them, thus a bound over a column that the subset
98
- // below leaves out still ranks them. An unknown bound column addresses nothing, exactly as an unknown
99
- // subset column does.
100
- const bound = reference.rowBound;
101
58
  const readRows = snapshotRows(artifact);
102
59
  if (readRows.isErr()) {
103
60
  return fail(reference, "unreadable-artifact", `the artifact at ${reference.path} holds a malformed row: ${readRows.error}`);
104
61
  }
105
- const allRows = readRows.value;
106
- if (bound !== undefined && columnsHeldByNoRow(allRows, [bound.column]).length > 0) {
107
- return fail(reference, "locator-out-of-range", `the row bound names column ${bound.column}, which the table at ${reference.path} does not hold`);
108
- }
109
- const rows = bound === undefined ? allRows : applyRowBound(allRows, bound);
110
- // The page states the shown count against the count of the artifact. This read is the one place that
111
- // holds both, thus a bounded table carries the pre-bound total out with its rows.
112
- const total = bound === undefined ? {} : { total: allRows.length };
113
- const columns = reference.columns;
114
- if (columns === undefined) {
115
- return ok({ type: "table", rows, ...total });
116
- }
117
- // A name that no row holds addresses nothing. Without this check a projection onto an invented column
118
- // gives an empty cell for each row and still resolves, which is the fabrication that grounding exists
119
- // to reject.
120
- const absent = columnsHeldByNoRow(rows, columns);
121
- if (absent.length > 0) {
122
- return fail(reference, "locator-out-of-range", `the table at ${reference.path} holds no column ${absent.join(", ")}`);
123
- }
124
- // Project each row onto the requested columns. A table tolerates a ragged row, thus a column that a
125
- // given row lacks is left out of that row and is not a failure.
126
- const projectedRows = rows.map((row) => {
127
- const projected = {};
128
- for (const column of columns) {
129
- const cell = row[column];
130
- if (cell !== undefined) {
131
- defineOwn(projected, column, cell);
132
- }
133
- }
134
- return projected;
135
- });
136
- return ok({ type: "table", rows: projectedRows, columns, ...total });
62
+ return readArtifactTable(reference, readRows.value);
137
63
  }
138
64
  function resolveArtifactFile(reference, snapshot) {
139
65
  const artifact = snapshotEntry(snapshot, reference.path);
@@ -145,58 +71,6 @@ function resolveArtifactFile(reference, snapshot) {
145
71
  }
146
72
  return ok({ type: "file", path: reference.path, hash: artifact.hash });
147
73
  }
148
- async function resolveDerivation(reference, snapshot) {
149
- const numbers = [];
150
- for (const input of reference.inputs) {
151
- const inputResult = await resolve(input, snapshot);
152
- if (inputResult.isErr()) {
153
- // Keep the inner reason so a reviewer sees the real cause, for example a missing artifact under
154
- // the derivation. The derivation itself did not fail on its own terms.
155
- return fail(reference, inputResult.error.reason, inputResult.error.detail);
156
- }
157
- const resolved = inputResult.value;
158
- const numeric = resolved.type === "scalar" ? asFiniteNumber(resolved.value) : undefined;
159
- if (numeric === undefined) {
160
- // The arithmetic needs a finite number. A cell that holds text does not address a usable
161
- // scalar, thus the closest reason is that the coordinate is out of range.
162
- return fail(reference, "locator-out-of-range", `input ${describeReference(input)} did not resolve to a finite number`);
163
- }
164
- numbers.push(numeric);
165
- }
166
- const a = numbers[0];
167
- const b = numbers[1];
168
- if (a === undefined || b === undefined) {
169
- return fail(reference, "locator-out-of-range", `operation ${reference.op} needs two inputs, and the derivation holds ${numbers.length}`);
170
- }
171
- let result;
172
- switch (reference.op) {
173
- case "ratio":
174
- result = a / b;
175
- break;
176
- case "delta":
177
- result = a - b;
178
- break;
179
- case "pctChange":
180
- // A fraction, not a percent. A change of one half gives 0.5, thus an author asserts 0.5 and a
181
- // `unit` of `%` only tells a renderer how to show it.
182
- result = (a - b) / b;
183
- break;
184
- }
185
- if (!Number.isFinite(result)) {
186
- return fail(reference, "locator-out-of-range", `operation ${reference.op} does not yield a finite value, because the divisor is zero`);
187
- }
188
- const valueFailure = checkValueAssertion(reference, reference.assert?.value, reference.assert?.tolerance, result);
189
- return valueFailure !== undefined ? err(valueFailure) : ok({ type: "scalar", value: result });
190
- }
191
- function resolveCitation(reference, snapshot) {
192
- const key = `${reference.idKind}:${reference.id}`;
193
- if (snapshot.citations === undefined || !snapshot.citations.includes(key)) {
194
- return fail(reference, "artifact-missing", `the citation ${key} is not in the pinned evidence`);
195
- }
196
- const value = { type: "citation", id: key };
197
- const citationFailure = checkCitationAssertion(reference, reference.assert?.value, key);
198
- return citationFailure !== undefined ? err(citationFailure) : ok(value);
199
- }
200
74
  async function resolve(reference, snapshot) {
201
75
  switch (reference.kind) {
202
76
  case "artifact-value":
@@ -206,7 +80,7 @@ async function resolve(reference, snapshot) {
206
80
  case "artifact-file":
207
81
  return resolveArtifactFile(reference, snapshot);
208
82
  case "derivation":
209
- return resolveDerivation(reference, snapshot);
83
+ return resolveDerivation(reference, snapshot, resolve);
210
84
  case "citation":
211
85
  return resolveCitation(reference, snapshot);
212
86
  }
@@ -13,9 +13,9 @@
13
13
  * holds the streamed hash and the parsed rows against a stat signature. Two tool calls of one analysis
14
14
  * share one cache.
15
15
  *
16
- * The realization calls the shared assert functions in `assert-rules.ts`, and it mirrors the locator walk
17
- * and the reason choices of `fixture-resolver.ts`. Thus one semantics exists, and the fixture stays the
18
- * executable specification of the value tier. The two realizations are substitutable behind the seam.
16
+ * The realization calls the shared assert functions in `assert-rules.ts` and the shared reference reads in
17
+ * `reference-read.ts`, as the fixture realization does. Thus one semantics exists, and the fixture stays
18
+ * the executable specification of the value tier. The two realizations are substitutable behind the seam.
19
19
  */
20
20
  import { type ReferenceResolver } from "./reference-resolver.js";
21
21
  /** One artifact row, as plain cells. A text-backed format holds a numeric column as a string. */