@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.
- package/dist/agents/sandbox/index.js +3 -0
- package/dist/agents/sandbox/mdv-preparation-agent.d.ts +9 -0
- package/dist/agents/sandbox/mdv-preparation-agent.js +37 -0
- package/dist/app/chat-turn-workflow.js +1 -0
- package/dist/app/chat-turn.d.ts +4 -2
- package/dist/app/chat-turn.js +46 -46
- package/dist/app/work.d.ts +7 -3
- package/dist/app/work.js +17 -16
- package/dist/contracts/chat-parts.d.ts +46 -5
- package/dist/contracts/chat-parts.js +2 -1
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/part-registry.js +1 -0
- package/dist/contracts/schemas/chat-parts.d.ts +168 -4
- package/dist/contracts/schemas/chat-parts.js +21 -1
- package/dist/index.d.ts +1 -1
- package/dist/lib/regulatory-corpus/fetch-ich.js +1 -16
- package/dist/lib/slug.d.ts +18 -0
- package/dist/lib/slug.js +29 -0
- package/dist/lib/test/pdf-fixture.d.ts +5 -0
- package/dist/lib/test/pdf-fixture.js +14 -0
- package/dist/loop/run-loop.js +23 -35
- package/dist/loop/sub-agent.d.ts +2 -2
- package/dist/loop/sub-agent.js +1 -1
- package/dist/loop/test/metric-reader.d.ts +9 -0
- package/dist/loop/test/metric-reader.js +33 -0
- package/dist/loop/test/unnamed-model.d.ts +6 -0
- package/dist/loop/test/unnamed-model.js +24 -0
- package/dist/memory/conversation-display-recorder.d.ts +2 -0
- package/dist/memory/conversation-display-recorder.js +6 -2
- package/dist/memory/conversation-display-storage.d.ts +4 -2
- package/dist/memory/conversation-display-storage.js +19 -1
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +6 -4
- package/dist/prompts/sandbox/mdv-preparation-agent.d.ts +1 -0
- package/dist/prompts/sandbox/mdv-preparation-agent.js +56 -0
- package/dist/report-model/fixture-resolver.js +9 -135
- package/dist/report-model/production-resolver.d.ts +3 -3
- package/dist/report-model/production-resolver.js +12 -127
- package/dist/report-model/reference-read.d.ts +38 -0
- package/dist/report-model/reference-read.js +149 -0
- package/dist/report-model/reference-resolver.d.ts +7 -0
- package/dist/report-model/reference-resolver.js +1 -1
- package/dist/report-render/chart.js +48 -46
- package/dist/report-render/figures/common.d.ts +34 -0
- package/dist/report-render/figures/common.js +4 -1
- package/dist/report-render/figures/equal-units.js +2 -4
- package/dist/report-render/figures/sankey.js +1 -3
- package/dist/report-render/figures/upset.js +2 -5
- package/dist/runtime/assemble.js +2 -1
- package/dist/sandbox/execution-id.js +2 -14
- package/dist/sandbox/leases.js +46 -9
- package/dist/schemas/validate-plan.js +2 -14
- package/dist/state/active-turns.d.ts +6 -2
- package/dist/state/active-turns.js +11 -7
- package/dist/state/data-profile.d.ts +8 -0
- package/dist/state/data-profile.js +18 -0
- package/dist/state/index.d.ts +1 -1
- package/dist/state/index.js +1 -1
- package/dist/state/migrations/20261102120000_record_the_waiter_of_a_pause.d.ts +6 -0
- package/dist/state/migrations/20261102120000_record_the_waiter_of_a_pause.js +10 -0
- package/dist/state/migrations/index.js +2 -0
- package/dist/state/paused-work.d.ts +5 -3
- package/dist/state/paused-work.js +11 -7
- package/dist/state/runs.d.ts +3 -2
- package/dist/state/runs.js +4 -3
- package/dist/tasks/data-profile-index.js +3 -17
- package/dist/tasks/data-profile-resolve.js +3 -17
- package/dist/tasks/data-profile.js +1 -0
- package/dist/tools/lib/opentargets-client.js +15 -25
- package/dist/tools/report-authoring/authoring-tools.d.ts +3 -3
- package/dist/tools/report-authoring/authoring-tools.js +7 -6
- package/dist/tools/research/generate-analogy-report.js +10 -2
- package/dist/workflows/execute-analysis.js +4 -3
- package/dist/workflows/pause.d.ts +3 -3
- package/dist/workflows/pause.js +15 -9
- package/dist/workflows/sandbox-step.js +1 -0
- 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
|
|
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
|
|
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.
|
|
78
|
-
|
|
79
|
-
|
|
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 {
|
|
8
|
-
import {
|
|
9
|
-
|
|
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
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
17
|
-
*
|
|
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
|
|
17
|
-
*
|
|
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 {
|
|
30
|
-
import {
|
|
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
|
-
|
|
542
|
-
if (
|
|
543
|
-
|
|
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 =
|
|
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
|
-
|
|
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>;
|