@inflexa-ai/harness 0.48.3 → 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 (77) 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.js +1 -0
  5. package/dist/app/chat-turn.d.ts +4 -2
  6. package/dist/app/chat-turn.js +46 -46
  7. package/dist/app/work.d.ts +7 -3
  8. package/dist/app/work.js +17 -16
  9. package/dist/contracts/chat-parts.d.ts +46 -5
  10. package/dist/contracts/chat-parts.js +2 -1
  11. package/dist/contracts/index.d.ts +1 -1
  12. package/dist/contracts/part-registry.js +1 -0
  13. package/dist/contracts/schemas/chat-parts.d.ts +168 -4
  14. package/dist/contracts/schemas/chat-parts.js +21 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/lib/regulatory-corpus/fetch-ich.js +1 -16
  17. package/dist/lib/slug.d.ts +18 -0
  18. package/dist/lib/slug.js +29 -0
  19. package/dist/lib/test/pdf-fixture.d.ts +5 -0
  20. package/dist/lib/test/pdf-fixture.js +14 -0
  21. package/dist/loop/run-loop.js +23 -35
  22. package/dist/loop/sub-agent.d.ts +2 -2
  23. package/dist/loop/sub-agent.js +1 -1
  24. package/dist/loop/test/metric-reader.d.ts +9 -0
  25. package/dist/loop/test/metric-reader.js +33 -0
  26. package/dist/loop/test/unnamed-model.d.ts +6 -0
  27. package/dist/loop/test/unnamed-model.js +24 -0
  28. package/dist/memory/conversation-display-recorder.d.ts +2 -0
  29. package/dist/memory/conversation-display-recorder.js +6 -2
  30. package/dist/memory/conversation-display-storage.d.ts +4 -2
  31. package/dist/memory/conversation-display-storage.js +19 -1
  32. package/dist/prompts/conversation.d.ts +1 -1
  33. package/dist/prompts/conversation.js +6 -4
  34. package/dist/prompts/sandbox/mdv-preparation-agent.d.ts +1 -0
  35. package/dist/prompts/sandbox/mdv-preparation-agent.js +56 -0
  36. package/dist/report-model/fixture-resolver.js +9 -135
  37. package/dist/report-model/production-resolver.d.ts +3 -3
  38. package/dist/report-model/production-resolver.js +12 -127
  39. package/dist/report-model/reference-read.d.ts +38 -0
  40. package/dist/report-model/reference-read.js +149 -0
  41. package/dist/report-model/reference-resolver.d.ts +7 -0
  42. package/dist/report-model/reference-resolver.js +1 -1
  43. package/dist/report-render/chart.js +48 -46
  44. package/dist/report-render/figures/common.d.ts +34 -0
  45. package/dist/report-render/figures/common.js +4 -1
  46. package/dist/report-render/figures/equal-units.js +2 -4
  47. package/dist/report-render/figures/sankey.js +1 -3
  48. package/dist/report-render/figures/upset.js +2 -5
  49. package/dist/runtime/assemble.js +2 -1
  50. package/dist/sandbox/execution-id.js +2 -14
  51. package/dist/sandbox/leases.js +46 -9
  52. package/dist/schemas/validate-plan.js +2 -14
  53. package/dist/state/active-turns.d.ts +6 -2
  54. package/dist/state/active-turns.js +11 -7
  55. package/dist/state/data-profile.d.ts +8 -0
  56. package/dist/state/data-profile.js +18 -0
  57. package/dist/state/index.d.ts +1 -1
  58. package/dist/state/index.js +1 -1
  59. package/dist/state/migrations/20261102120000_record_the_waiter_of_a_pause.d.ts +6 -0
  60. package/dist/state/migrations/20261102120000_record_the_waiter_of_a_pause.js +10 -0
  61. package/dist/state/migrations/index.js +2 -0
  62. package/dist/state/paused-work.d.ts +5 -3
  63. package/dist/state/paused-work.js +11 -7
  64. package/dist/state/runs.d.ts +3 -2
  65. package/dist/state/runs.js +4 -3
  66. package/dist/tasks/data-profile-index.js +3 -17
  67. package/dist/tasks/data-profile-resolve.js +3 -17
  68. package/dist/tasks/data-profile.js +1 -0
  69. package/dist/tools/lib/opentargets-client.js +15 -25
  70. package/dist/tools/report-authoring/authoring-tools.d.ts +3 -3
  71. package/dist/tools/report-authoring/authoring-tools.js +7 -6
  72. package/dist/tools/research/generate-analogy-report.js +10 -2
  73. package/dist/workflows/execute-analysis.js +4 -3
  74. package/dist/workflows/pause.d.ts +3 -3
  75. package/dist/workflows/pause.js +15 -9
  76. package/dist/workflows/sandbox-step.js +1 -0
  77. package/package.json +7 -4
@@ -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, ModelRefusalPayloadSchema, 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 }),
@@ -78,6 +79,7 @@ const dataSchemas = {
78
79
  "report-rendered": ReportRenderedPartSchema.omit({ type: true }),
79
80
  compaction: CompactionPartSchema.omit({ type: true }),
80
81
  "model-refusal": ModelRefusalPayloadSchema,
82
+ pause: PausePartSchema.omit({ type: true }),
81
83
  "subagent-activity": SubAgentActivityPartSchema.omit({ type: true }).extend({
82
84
  entries: z.array(SubAgentActivityEntrySchema.extend({ outcome: ToolCallOutcomeSchema })),
83
85
  }),
@@ -175,6 +177,7 @@ export async function parseStoredDisplayEnvelope(value, identity, logger) {
175
177
  const STORED_SOURCE_TYPES = [
176
178
  "tool-call",
177
179
  "data-presentation",
180
+ "data-analogy-report",
178
181
  "data-plan",
179
182
  "data-run-card",
180
183
  "data-plan-approval",
@@ -184,6 +187,7 @@ const STORED_SOURCE_TYPES = [
184
187
  "data-report-rendered",
185
188
  "data-compaction",
186
189
  "data-model-refusal",
190
+ "data-pause",
187
191
  "data-subagent-activity",
188
192
  ];
189
193
  const STORED_SOURCE_TYPE_SET = new Set(STORED_SOURCE_TYPES);
@@ -225,6 +229,10 @@ export function conversationDisplayPart(part) {
225
229
  const { type: _type, ...data } = part;
226
230
  return { type: "data-presentation", id, data };
227
231
  }
232
+ case "data-analogy-report": {
233
+ const { type: _type, ...data } = part;
234
+ return { type: "data-analogy-report", id, data };
235
+ }
228
236
  case "data-plan": {
229
237
  const { type: _type, ...data } = part;
230
238
  return { type: "data-plan", id, data };
@@ -261,6 +269,10 @@ export function conversationDisplayPart(part) {
261
269
  const { type: _type, ...data } = part;
262
270
  return { type: "data-model-refusal", id, data };
263
271
  }
272
+ case "data-pause": {
273
+ const { type: _type, ...data } = part;
274
+ return { type: "data-pause", id, data };
275
+ }
264
276
  case "data-subagent-activity": {
265
277
  const { type: _type, ...payload } = part;
266
278
  return { type: "data-subagent-activity", id, data: { ...payload, entries: part.entries.map(closedEntry) } };
@@ -297,6 +309,9 @@ export function conversationUIToChatMessages(messages) {
297
309
  case "data-presentation":
298
310
  parts.push({ type: "data-presentation", ...part.data });
299
311
  break;
312
+ case "data-analogy-report":
313
+ parts.push({ type: "data-analogy-report", ...part.data });
314
+ break;
300
315
  case "data-plan":
301
316
  parts.push({ type: "data-plan", ...part.data });
302
317
  break;
@@ -324,6 +339,9 @@ export function conversationUIToChatMessages(messages) {
324
339
  case "data-model-refusal":
325
340
  parts.push({ type: "data-model-refusal", ...part.data });
326
341
  break;
342
+ case "data-pause":
343
+ parts.push({ type: "data-pause", ...part.data });
344
+ break;
327
345
  case "data-subagent-activity":
328
346
  parts.push({ type: "data-subagent-activity", ...part.data });
329
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
+ `;
@@ -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. */
@@ -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 { err, ok } from "neverthrow";
21
21
  import { readFile, stat } from "node:fs/promises";
@@ -26,8 +26,9 @@ import { allWithConcurrency } from "../lib/async-utils.js";
26
26
  import { computeSha256File } from "../lib/fs-helpers.js";
27
27
  import { parseJsonAs } from "../lib/parse-json.js";
28
28
  import { resolveWorkspacePath } from "../workspace/paths.js";
29
- import { asFiniteNumber, cellMatchesFilterValue, checkCitationAssertion, checkValueAssertion } from "./assert-rules.js";
30
- import { applyRowBound, columnsHeldByNoRow, fileTypeHoldsNoCell, snapshotEntry, } from "./reference-resolver.js";
29
+ import { checkValueAssertion } from "./assert-rules.js";
30
+ import { fail, readArtifactTable, resolveCitation, resolveDerivation, selectRow } from "./reference-read.js";
31
+ import { fileTypeHoldsNoCell, snapshotEntry } from "./reference-resolver.js";
31
32
  /**
32
33
  * The default cap on the file size that the host fast path reads. A larger file takes the fall-through.
33
34
  * The cap gates the cell extraction only. The identity compare streams the hash at every size.
@@ -95,15 +96,6 @@ export function createArtifactReadStore() {
95
96
  function fallThroughCells(path, hostDetail) {
96
97
  return { kind: "unavailable", detail: `the host read of the file at ${path} stopped because ${hostDetail}, and no other reader exists` };
97
98
  }
98
- /** Build an `Err` that carries the unresolved reference. The `detail` key is present only when there is a detail to carry. */
99
- function fail(reference, reason, detail) {
100
- const failure = detail !== undefined ? { reference, reason, detail } : { reference, reason };
101
- return err(failure);
102
- }
103
- /** A short account of a derivation input, for a detail that names which input broke the arithmetic. */
104
- function describeReference(reference) {
105
- return `artifact value at ${reference.path} column ${reference.locator.column}`;
106
- }
107
99
  /**
108
100
  * Map a file extension to the format that the host arm reads. An unknown extension gives back `undefined`.
109
101
  *
@@ -538,36 +530,15 @@ export function createProductionResolver(deps) {
538
530
  /** Walk the locator over the rows, and match the resolved cell against the authored belief. */
539
531
  function selectCell(reference, rows) {
540
532
  const locator = reference.locator;
541
- let selectedRow;
542
- if (locator.row !== undefined) {
543
- const row = rows[locator.row];
544
- if (row === undefined) {
545
- return fail(reference, "locator-out-of-range", `row ${locator.row} is outside the ${rows.length} rows`);
546
- }
547
- selectedRow = row;
548
- }
549
- else if (locator.rowFilter !== undefined) {
550
- const filter = locator.rowFilter;
551
- const matches = rows.filter((row) => cellMatchesFilterValue(row[filter.column], filter.value));
552
- const match = matches[0];
553
- if (match === undefined) {
554
- return fail(reference, "locator-out-of-range", `no row where ${filter.column} equals ${String(filter.value)}`);
555
- }
556
- if (matches.length > 1) {
557
- return fail(reference, "ambiguous-match", `${matches.length} rows where ${filter.column} equals ${String(filter.value)}`);
558
- }
559
- selectedRow = match;
560
- }
561
- else {
562
- // The locator schema forbids a locator with neither selector. The resolver cannot depend on a
563
- // prior parse, thus it treats a row that no selector addresses as out of range.
564
- return fail(reference, "locator-out-of-range", "the locator selects no row");
533
+ const selection = selectRow(rows, locator);
534
+ if (selection.isErr()) {
535
+ return fail(reference, selection.error.reason, selection.error.detail);
565
536
  }
566
537
  // A real table holds an empty cell, and a parse gives it back as an absent key. An absent key is
567
538
  // not a value that a scalar reference can bind. An empty string is a value, thus it stays valid.
568
539
  // A cell that parsed as `null` never lands in a row: `coerceCell` drops it, so `null` is not
569
540
  // an absence the resolver can meet here.
570
- const cell = selectedRow[locator.column];
541
+ const cell = selection.value[locator.column];
571
542
  if (cell === undefined) {
572
543
  return fail(reference, "locator-out-of-range", `column ${locator.column} holds no value in the selected row`);
573
544
  }
@@ -583,41 +554,7 @@ export function createProductionResolver(deps) {
583
554
  if (gated.isErr()) {
584
555
  return err(gated.error);
585
556
  }
586
- // The bound ranks the rows as the artifact holds them, thus a bound over a column that the subset
587
- // below leaves out still ranks them. An unknown bound column addresses nothing, exactly as an
588
- // unknown subset column does.
589
- const bound = reference.rowBound;
590
- const allRows = gated.value;
591
- if (bound !== undefined && columnsHeldByNoRow(allRows, [bound.column]).length > 0) {
592
- return fail(reference, "locator-out-of-range", `the row bound names column ${bound.column}, which the table at ${reference.path} does not hold`);
593
- }
594
- const rows = bound === undefined ? allRows : applyRowBound(allRows, bound);
595
- // The page states the shown count against the count of the artifact. This read is the one place that
596
- // holds both, thus a bounded table carries the pre-bound total out with its rows.
597
- const total = bound === undefined ? {} : { total: allRows.length };
598
- const columns = reference.columns;
599
- if (columns === undefined) {
600
- return ok({ type: "table", rows, ...total });
601
- }
602
- // A name that no row holds addresses nothing. Without this check a projection onto an invented
603
- // column gives an empty cell for each row and still resolves, which grounding exists to reject.
604
- const absent = columnsHeldByNoRow(rows, columns);
605
- if (absent.length > 0) {
606
- return fail(reference, "locator-out-of-range", `the table at ${reference.path} holds no column ${absent.join(", ")}`);
607
- }
608
- // Project each row onto the requested columns. A table tolerates a ragged row, thus a column that
609
- // a given row lacks is left out of that row and is not a failure.
610
- const projectedRows = rows.map((row) => {
611
- const projected = {};
612
- for (const column of columns) {
613
- const cell = row[column];
614
- if (cell !== undefined) {
615
- projected[column] = cell;
616
- }
617
- }
618
- return projected;
619
- });
620
- return ok({ type: "table", rows: projectedRows, columns, ...total });
557
+ return readArtifactTable(reference, gated.value);
621
558
  }
622
559
  async function resolveArtifactFile(reference, snapshot) {
623
560
  const identity = await getIdentity(reference.path, snapshot);
@@ -632,58 +569,6 @@ export function createProductionResolver(deps) {
632
569
  }
633
570
  return ok({ type: "file", path: reference.path, hash: identity.onDiskHash });
634
571
  }
635
- async function resolveDerivation(reference, snapshot) {
636
- const numbers = [];
637
- for (const input of reference.inputs) {
638
- const inputResult = await resolveArtifactValue(input, snapshot);
639
- if (inputResult.isErr()) {
640
- // Keep the inner reason so a reviewer sees the real cause, for example a missing artifact
641
- // under the derivation. The derivation itself did not fail on its own terms.
642
- return fail(reference, inputResult.error.reason, inputResult.error.detail);
643
- }
644
- const resolved = inputResult.value;
645
- const numeric = resolved.type === "scalar" ? asFiniteNumber(resolved.value) : undefined;
646
- if (numeric === undefined) {
647
- // The arithmetic needs a finite number. A cell that holds text does not address a usable
648
- // scalar, thus the closest reason is that the coordinate is out of range.
649
- return fail(reference, "locator-out-of-range", `input ${describeReference(input)} did not resolve to a finite number`);
650
- }
651
- numbers.push(numeric);
652
- }
653
- const a = numbers[0];
654
- const b = numbers[1];
655
- if (a === undefined || b === undefined) {
656
- return fail(reference, "locator-out-of-range", `operation ${reference.op} needs two inputs, and the derivation holds ${numbers.length}`);
657
- }
658
- let result;
659
- switch (reference.op) {
660
- case "ratio":
661
- result = a / b;
662
- break;
663
- case "delta":
664
- result = a - b;
665
- break;
666
- case "pctChange":
667
- // A fraction, not a percent. A change of one half gives 0.5, thus an author asserts 0.5 and
668
- // a `unit` of `%` only tells a renderer how to show it.
669
- result = (a - b) / b;
670
- break;
671
- }
672
- if (!Number.isFinite(result)) {
673
- return fail(reference, "locator-out-of-range", `operation ${reference.op} does not yield a finite value, because the divisor is zero`);
674
- }
675
- const valueFailure = checkValueAssertion(reference, reference.assert?.value, reference.assert?.tolerance, result);
676
- return valueFailure !== undefined ? err(valueFailure) : ok({ type: "scalar", value: result });
677
- }
678
- function resolveCitation(reference, snapshot) {
679
- const key = `${reference.idKind}:${reference.id}`;
680
- if (snapshot.citations === undefined || !snapshot.citations.includes(key)) {
681
- return fail(reference, "artifact-missing", `the citation ${key} is not in the pinned evidence`);
682
- }
683
- const value = { type: "citation", id: key };
684
- const citationFailure = checkCitationAssertion(reference, reference.assert?.value, key);
685
- return citationFailure !== undefined ? err(citationFailure) : ok(value);
686
- }
687
572
  async function resolve(reference, snapshot) {
688
573
  switch (reference.kind) {
689
574
  case "artifact-value":
@@ -693,7 +578,7 @@ export function createProductionResolver(deps) {
693
578
  case "artifact-file":
694
579
  return resolveArtifactFile(reference, snapshot);
695
580
  case "derivation":
696
- return resolveDerivation(reference, snapshot);
581
+ return resolveDerivation(reference, snapshot, resolveArtifactValue);
697
582
  case "citation":
698
583
  return resolveCitation(reference, snapshot);
699
584
  }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The reads of a report reference that the two resolver realizations share.
3
+ *
4
+ * The fixture resolver reads the rows of a snapshot, and the production resolver reads the rows of the
5
+ * pinned artifact on disk. What both do with those rows — the locator walk, the table read, and the
6
+ * derivation arithmetic — lives here, so one semantics exists for both realizations.
7
+ */
8
+ import { type Result } from "neverthrow";
9
+ import type { ArtifactTableReference, ArtifactValueReference, CitationReference, DerivationInputReference, DerivationReference, Reference, UnresolvedReason, UnresolvedReference } from "../contracts/report-reference.js";
10
+ import { type ReportSnapshot, type ResolvedValue } from "./reference-resolver.js";
11
+ /** Build an `Err` that carries the unresolved reference. The `detail` key is present only when there is a detail to carry. */
12
+ export declare function fail(reference: Reference, reason: UnresolvedReason, detail?: string): Result<ResolvedValue, UnresolvedReference>;
13
+ /** The failure of a row selection: the reason that an `UnresolvedReference` carries, and its detail. */
14
+ export type RowSelectionFailure = {
15
+ reason: "locator-out-of-range" | "ambiguous-match";
16
+ detail: string;
17
+ };
18
+ /**
19
+ * Walk a locator over the rows, and give back the one row that it selects, or the failure.
20
+ *
21
+ * A row index selects the row at that position. A row filter selects the one row that matches; no match
22
+ * selects nothing, and two or more matches are ambiguous.
23
+ */
24
+ export declare function selectRow(rows: Array<Record<string, string | number>>, locator: ArtifactValueReference["locator"]): Result<Record<string, string | number>, RowSelectionFailure>;
25
+ /**
26
+ * Read the table that a table reference addresses out of rows that the caller read from the artifact:
27
+ * cut the rows on the row bound, refuse a column that no row holds, and project the requested columns.
28
+ */
29
+ export declare function readArtifactTable(reference: ArtifactTableReference, allRows: Array<Record<string, string | number>>): Result<ResolvedValue, UnresolvedReference>;
30
+ /**
31
+ * Resolve a derivation: read each input with `resolveInput`, then run the arithmetic of the operation.
32
+ *
33
+ * The read of an input stays with the realization: the fixture resolver reads its snapshot, and the
34
+ * production resolver reads the pinned artifact on disk.
35
+ */
36
+ export declare function resolveDerivation(reference: DerivationReference, snapshot: ReportSnapshot, resolveInput: (input: DerivationInputReference, snapshot: ReportSnapshot) => Promise<Result<ResolvedValue, UnresolvedReference>>): Promise<Result<ResolvedValue, UnresolvedReference>>;
37
+ /** Resolve a citation: confirm that the id is in the pinned evidence, and echo the key back. */
38
+ export declare function resolveCitation(reference: CitationReference, snapshot: ReportSnapshot): Result<ResolvedValue, UnresolvedReference>;