@inflexa-ai/harness 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/report-builder.d.ts +2 -0
- package/dist/agents/report-builder.js +2 -0
- package/dist/agents/report-session-agent.d.ts +56 -0
- package/dist/agents/report-session-agent.js +72 -0
- package/dist/app/report-session-runtime.d.ts +72 -0
- package/dist/app/report-session-runtime.js +157 -0
- package/dist/app/spawn-report-session.d.ts +85 -0
- package/dist/app/spawn-report-session.js +83 -0
- package/dist/contracts/chat-events.d.ts +9 -0
- package/dist/contracts/index.d.ts +6 -2
- package/dist/contracts/index.js +3 -1
- package/dist/contracts/organ-system.d.ts +49 -0
- package/dist/contracts/organ-system.js +67 -0
- package/dist/contracts/report-blocks.d.ts +564 -0
- package/dist/contracts/report-blocks.js +117 -0
- package/dist/contracts/report-reference.d.ts +459 -0
- package/dist/contracts/report-reference.js +256 -0
- package/dist/contracts/schemas/chat-events.d.ts +2 -0
- package/dist/contracts/schemas/chat-events.js +2 -0
- package/dist/contracts/severity.d.ts +15 -0
- package/dist/contracts/severity.js +10 -0
- package/dist/contracts/target-dossier.d.ts +8675 -12972
- package/dist/contracts/target-dossier.js +534 -591
- package/dist/data/safety-panel-schema.d.ts +18 -5
- package/dist/data/safety-panel-schema.js +4 -4
- package/dist/execution/artifact-metadata.js +1 -0
- package/dist/execution/report-runner.js +1 -3
- package/dist/execution/run-synthesis.js +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +19 -0
- package/dist/lib/async-utils.d.ts +16 -4
- package/dist/lib/async-utils.js +20 -15
- package/dist/lib/host-concurrency.js +1 -0
- package/dist/loop/__fixtures__/scripted-provider.js +23 -2
- package/dist/loop/run-agent.js +58 -10
- package/dist/loop/tool-detail.d.ts +40 -0
- package/dist/loop/tool-detail.js +35 -3
- package/dist/loop/types.d.ts +13 -0
- package/dist/memory/thread-history.d.ts +10 -0
- package/dist/memory/thread-history.js +12 -1
- package/dist/memory/thread-store.d.ts +4 -4
- package/dist/memory/thread-store.js +18 -9
- package/dist/prompts/report-session.d.ts +1 -0
- package/dist/prompts/report-session.js +89 -0
- package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/executive-recommendation.js +5 -5
- package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/liability-bullets.js +1 -1
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +1 -1
- package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +11 -3
- package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/adversarial-critique.js +67 -0
- package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/claim-reverification.js +49 -0
- package/dist/prompts/target-assessment/investigation/index.d.ts +3 -0
- package/dist/prompts/target-assessment/investigation/index.js +3 -0
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +1 -0
- package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +49 -0
- package/dist/prompts/target-assessment/tox-voice/index.d.ts +1 -1
- package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +8 -5
- package/dist/prompts/target-assessment/tox-voice/vocabulary.js +9 -20
- package/dist/providers/__fixtures__/session.d.ts +5 -4
- package/dist/providers/__fixtures__/session.js +4 -4
- package/dist/providers/ai-sdk.d.ts +77 -0
- package/dist/providers/ai-sdk.js +149 -19
- package/dist/providers/errors.d.ts +14 -0
- package/dist/providers/errors.js +103 -18
- package/dist/providers/types.d.ts +16 -0
- package/dist/providers/types.js +10 -1
- package/dist/report-model/block-walk.d.ts +56 -0
- package/dist/report-model/block-walk.js +89 -0
- package/dist/report-model/draft-finish.d.ts +60 -0
- package/dist/report-model/draft-finish.js +56 -0
- package/dist/report-model/draft-operations.d.ts +125 -0
- package/dist/report-model/draft-operations.js +341 -0
- package/dist/report-model/draft-read.d.ts +59 -0
- package/dist/report-model/draft-read.js +109 -0
- package/dist/report-model/draft.d.ts +51 -0
- package/dist/report-model/draft.js +32 -0
- package/dist/report-model/fixture-resolver.d.ts +8 -0
- package/dist/report-model/fixture-resolver.js +252 -0
- package/dist/report-model/pin-snapshot.d.ts +30 -0
- package/dist/report-model/pin-snapshot.js +40 -0
- package/dist/report-model/reference-resolver.d.ts +113 -0
- package/dist/report-model/reference-resolver.js +60 -0
- package/dist/report-model/structural-validation.d.ts +22 -0
- package/dist/report-model/structural-validation.js +68 -0
- package/dist/report-model/validate.d.ts +72 -0
- package/dist/report-model/validate.js +95 -0
- package/dist/report-render/chart.d.ts +33 -0
- package/dist/report-render/chart.js +538 -0
- package/dist/report-render/page.d.ts +127 -0
- package/dist/report-render/page.js +307 -0
- package/dist/report-render/references.d.ts +32 -0
- package/dist/report-render/references.js +44 -0
- package/dist/report-render/render.d.ts +25 -0
- package/dist/report-render/render.js +125 -0
- package/dist/report-render/script-json.d.ts +13 -0
- package/dist/report-render/script-json.js +15 -0
- package/dist/report-render/types.d.ts +39 -0
- package/dist/report-render/types.js +10 -0
- package/dist/report-render/value-bridge.d.ts +62 -0
- package/dist/report-render/value-bridge.js +82 -0
- package/dist/report-render/views/chart-view.d.ts +11 -0
- package/dist/report-render/views/chart-view.js +23 -0
- package/dist/report-render/views/page-view.d.ts +20 -0
- package/dist/report-render/views/page-view.js +49 -0
- package/dist/report-render/views/prose.d.ts +28 -0
- package/dist/report-render/views/prose.js +75 -0
- package/dist/report-render/views/references-view.d.ts +20 -0
- package/dist/report-render/views/references-view.js +64 -0
- package/dist/report-render/views/values.d.ts +42 -0
- package/dist/report-render/views/values.js +43 -0
- package/dist/runtime/assemble.d.ts +15 -0
- package/dist/runtime/assemble.js +28 -6
- package/dist/runtime/boot.d.ts +3 -3
- package/dist/runtime/boot.js +15 -4
- package/dist/runtime/dbos.d.ts +4 -4
- package/dist/state/artifacts.d.ts +16 -0
- package/dist/state/artifacts.js +18 -0
- package/dist/state/init.js +54 -0
- package/dist/state/purge-analysis.js +2 -0
- package/dist/state/report-session-state.d.ts +112 -0
- package/dist/state/report-session-state.js +150 -0
- package/dist/state/report-versions.d.ts +129 -0
- package/dist/state/report-versions.js +167 -0
- package/dist/state/snapshot-parse.d.ts +34 -0
- package/dist/state/snapshot-parse.js +82 -0
- package/dist/tasks/data-profile.js +1 -0
- package/dist/tools/ad-hoc-router.d.ts +7 -2
- package/dist/tools/ad-hoc-router.js +2 -1
- package/dist/tools/bio/chembl.js +1 -0
- package/dist/tools/bio/comptox.js +1 -0
- package/dist/tools/bio/drug-gene-interactions.js +1 -0
- package/dist/tools/bio/gene-disease-evidence.js +1 -0
- package/dist/tools/bio/gene-preclinical-profile.js +1 -0
- package/dist/tools/bio/keys.d.ts +3 -2
- package/dist/tools/bio/keys.js +3 -2
- package/dist/tools/bio/lookup-annotation.js +1 -0
- package/dist/tools/bio/opentargets.d.ts +3 -3
- package/dist/tools/bio/opentargets.js +4 -3
- package/dist/tools/bio/pubchem.js +1 -0
- package/dist/tools/bio/search-clinical-trials.js +1 -0
- package/dist/tools/bio/search-faers.js +1 -0
- package/dist/tools/bio/search-geo-datasets.js +1 -0
- package/dist/tools/bio/search-interactions.js +1 -0
- package/dist/tools/bio/target-safety.d.ts +4 -6
- package/dist/tools/bio/target-safety.js +7 -6
- package/dist/tools/define-tool.d.ts +19 -12
- package/dist/tools/define-tool.js +4 -2
- package/dist/tools/display/show-user.js +1 -0
- package/dist/tools/iterate-report.js +2 -0
- package/dist/tools/lib/chembl-client.d.ts +2 -0
- package/dist/tools/lib/chembl-client.js +1 -0
- package/dist/tools/lib/monarch-client.d.ts +42 -0
- package/dist/tools/lib/monarch-client.js +100 -0
- package/dist/tools/report/build-report.js +1 -0
- package/dist/tools/report/mint-preview-url.js +1 -0
- package/dist/tools/report/preview-snapshot.js +1 -0
- package/dist/tools/report/submit-report.js +1 -0
- package/dist/tools/report/version-fs.js +4 -0
- package/dist/tools/report-authoring/authoring-tools.d.ts +241 -0
- package/dist/tools/report-authoring/authoring-tools.js +460 -0
- package/dist/tools/report-session/index.d.ts +8 -0
- package/dist/tools/report-session/index.js +8 -0
- package/dist/tools/report-session/preview-report.d.ts +90 -0
- package/dist/tools/report-session/preview-report.js +245 -0
- package/dist/tools/research/context7-docs.js +2 -0
- package/dist/tools/research/generate-analogy-report.js +1 -0
- package/dist/tools/research/generate-plan.js +7 -2
- package/dist/tools/research/inspect-data-profile.js +4 -0
- package/dist/tools/research/literature-reviewer.js +1 -0
- package/dist/tools/research/search-arxiv.js +1 -0
- package/dist/tools/research/search-github-repos.js +1 -0
- package/dist/tools/research/search-semantic-scholar.js +1 -0
- package/dist/tools/sandbox/list-available-packages.js +23 -0
- package/dist/tools/sandbox/list-available-refs.js +36 -0
- package/dist/tools/sandbox/report-blocker.js +1 -0
- package/dist/tools/sandbox/skills.js +2 -0
- package/dist/tools/workspace/file-stat.js +1 -0
- package/dist/tools/workspace/show-file.js +2 -0
- package/dist/tools/workspace/show-plan.js +1 -0
- package/dist/workflows/execute-analysis.d.ts +5 -1
- package/dist/workflows/execute-analysis.js +5 -5
- package/dist/workflows/execute-target-assessment.d.ts +13 -2
- package/dist/workflows/execute-target-assessment.js +78 -16
- package/dist/workflows/target-assessment/assemblers/index.d.ts +1 -1
- package/dist/workflows/target-assessment/assemblers/index.js +1 -1
- package/dist/workflows/target-assessment/assemblers/literature.d.ts +9 -27
- package/dist/workflows/target-assessment/assemblers/literature.js +32 -48
- package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +17 -9
- package/dist/workflows/target-assessment/assemblers/orchestrator.js +195 -85
- package/dist/workflows/target-assessment/assemblers/safety.d.ts +70 -48
- package/dist/workflows/target-assessment/assemblers/safety.js +125 -42
- package/dist/workflows/target-assessment/collectors/index.d.ts +5 -2
- package/dist/workflows/target-assessment/collectors/index.js +48 -5
- package/dist/workflows/target-assessment/coverage.d.ts +43 -27
- package/dist/workflows/target-assessment/coverage.js +33 -29
- package/dist/workflows/target-assessment/fanout/index.js +1 -0
- package/dist/workflows/target-assessment/investigation/index.d.ts +97 -0
- package/dist/workflows/target-assessment/investigation/index.js +419 -0
- package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +26 -5
- package/dist/workflows/target-assessment/lib/approval-precedents.js +95 -29
- package/dist/workflows/target-assessment/lib/citation-validator.js +1 -1
- package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +2 -2
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +2 -3
- package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -4
- package/dist/workflows/target-assessment/lib/compute-derived.d.ts +6 -5
- package/dist/workflows/target-assessment/lib/compute-derived.js +14 -5
- package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +5 -6
- package/dist/workflows/target-assessment/lib/family-complex-supplement.js +2 -3
- package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +72 -0
- package/dist/workflows/target-assessment/lib/fda-label-safety.js +166 -0
- package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -9
- package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -10
- package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +34 -0
- package/dist/workflows/target-assessment/lib/hpo-organ-map.js +63 -0
- package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +19 -0
- package/dist/workflows/target-assessment/lib/impc-organ-map.js +39 -0
- package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +24 -14
- package/dist/workflows/target-assessment/lib/meddra-organ-map.js +59 -47
- package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +1 -1
- package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +2 -3
- package/dist/workflows/target-assessment/lib/regulatory-actions.js +33 -4
- package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +44 -0
- package/dist/workflows/target-assessment/lib/safety-corroboration.js +273 -0
- package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +5 -7
- package/dist/workflows/target-assessment/lib/target-identity-filter.js +5 -7
- package/dist/workflows/target-assessment/metrics.d.ts +1 -6
- package/dist/workflows/target-assessment/metrics.js +1 -13
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +2147 -807
- package/dist/workflows/target-assessment/phase4-assemble.js +19 -19
- package/dist/workflows/target-assessment/phase5-persist.d.ts +20 -21
- package/dist/workflows/target-assessment/phase5-persist.js +56 -173
- package/dist/workflows/target-assessment/schemas.d.ts +46 -1
- package/dist/workflows/target-assessment/schemas.js +19 -0
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +1 -0
- package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +2 -0
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +26 -62
- package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -4
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +27 -154
- package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -6
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -6
- package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -18
- package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +1 -0
- package/dist/workflows/target-assessment/synthesis/index.d.ts +52 -5
- package/dist/workflows/target-assessment/synthesis/index.js +4 -1
- package/dist/workflows/target-assessment/synthesis-probes.d.ts +3 -3
- package/package.json +4 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A local, in-memory realization of the reference resolver. It reads only the snapshot that it is given,
|
|
3
|
+
* thus it needs no storage and no network. A test and a benchmark use it to exercise the resolution and
|
|
4
|
+
* assertion rules without a host.
|
|
5
|
+
*/
|
|
6
|
+
import { err, ok } from "neverthrow";
|
|
7
|
+
import { columnsHeldByNoRow, fileTypeHoldsNoCell, snapshotEntry, } from "./reference-resolver.js";
|
|
8
|
+
/**
|
|
9
|
+
* The relative epsilon of a value match with no authored tolerance.
|
|
10
|
+
*
|
|
11
|
+
* Float arithmetic shifts a computed value in its last bits. For example, `0.3 - 0.1` gives
|
|
12
|
+
* 0.19999999999999998, and exact equality would report that noise as a fabrication.
|
|
13
|
+
*
|
|
14
|
+
* The epsilon absorbs the noise and nothing else. It is far below the difference that a rounded figure
|
|
15
|
+
* makes, thus an author who writes 0.05 against a true 0.0499 still fails. An author who wants a rounded
|
|
16
|
+
* figure to pass states a `tolerance`, which is the one place where the intent belongs.
|
|
17
|
+
*/
|
|
18
|
+
const RELATIVE_EPSILON = 1e-9;
|
|
19
|
+
/** Build an `Err` that carries the unresolved reference. The `detail` key is present only when there is a detail to carry. */
|
|
20
|
+
function fail(reference, reason, detail) {
|
|
21
|
+
const failure = detail !== undefined ? { reference, reason, detail } : { reference, reason };
|
|
22
|
+
return err(failure);
|
|
23
|
+
}
|
|
24
|
+
/** A short account of a derivation input, for a detail that names which input broke the arithmetic. */
|
|
25
|
+
function describeReference(reference) {
|
|
26
|
+
return `artifact value at ${reference.path} column ${reference.locator.column}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read an authored value or a resolved cell as a finite number.
|
|
30
|
+
*
|
|
31
|
+
* A text-backed artifact such as a CSV holds every cell as a string, thus a numeric column arrives as
|
|
32
|
+
* `"0.01"` and not as `0.01`. A comparison that respects the JavaScript type would fail an author who
|
|
33
|
+
* states the number, and the arithmetic of a derivation could never run over a real CSV.
|
|
34
|
+
*
|
|
35
|
+
* The parse reads the whole string, thus `"12 genes"` stays text and never reads as 12. `Number` accepts
|
|
36
|
+
* the exponent form that a p-value uses, for example `"1.2e-45"`. A blank string is not a number, because
|
|
37
|
+
* `Number("")` gives 0 and an empty cell must never read as zero.
|
|
38
|
+
*/
|
|
39
|
+
function asFiniteNumber(value) {
|
|
40
|
+
if (typeof value === "number") {
|
|
41
|
+
return Number.isFinite(value) ? value : undefined;
|
|
42
|
+
}
|
|
43
|
+
if (value.trim() === "") {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const parsed = Number(value);
|
|
47
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
48
|
+
}
|
|
49
|
+
/** Name a value with its type, so that a failed match never reads as two identical values. */
|
|
50
|
+
function describeValue(value) {
|
|
51
|
+
return typeof value === "string" ? `the string ${JSON.stringify(value)}` : `the number ${value}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Match a resolved value against an authored one.
|
|
55
|
+
*
|
|
56
|
+
* The comparison is numeric whenever both sides read as a finite number, thus the CSV cell `"0.01"`
|
|
57
|
+
* matches the number 0.01 that an author states. An authored tolerance is an absolute difference, and it
|
|
58
|
+
* is the author's own statement of how close is close enough. With no tolerance the comparison is
|
|
59
|
+
* relative, because an exact match would fail on the float noise of a computed value. Any other pair
|
|
60
|
+
* matches on exact equality.
|
|
61
|
+
*/
|
|
62
|
+
function valuesMatch(expected, actual, tolerance) {
|
|
63
|
+
const expectedNumber = asFiniteNumber(expected);
|
|
64
|
+
const actualNumber = asFiniteNumber(actual);
|
|
65
|
+
if (expectedNumber !== undefined && actualNumber !== undefined) {
|
|
66
|
+
if (tolerance !== undefined) {
|
|
67
|
+
return Math.abs(expectedNumber - actualNumber) <= tolerance;
|
|
68
|
+
}
|
|
69
|
+
return Math.abs(expectedNumber - actualNumber) <= RELATIVE_EPSILON * Math.max(1, Math.abs(expectedNumber), Math.abs(actualNumber));
|
|
70
|
+
}
|
|
71
|
+
return expected === actual;
|
|
72
|
+
}
|
|
73
|
+
/** Match a resolved scalar against the authored value, under the authored tolerance. */
|
|
74
|
+
function checkValueAssertion(reference, expected, tolerance, resolved) {
|
|
75
|
+
if (expected === undefined || valuesMatch(expected, resolved, tolerance)) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return { reference, reason: "assertion-failed", detail: `expected ${describeValue(expected)} but resolved ${describeValue(resolved)}` };
|
|
79
|
+
}
|
|
80
|
+
/** Match the resolved citation key against the authored value. The key carries its `idKind:` prefix. */
|
|
81
|
+
function checkCitationAssertion(reference, expected, resolved) {
|
|
82
|
+
if (expected === undefined || expected === resolved) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return { reference, reason: "assertion-failed", detail: `expected ${String(expected)} but resolved citation ${resolved}` };
|
|
86
|
+
}
|
|
87
|
+
function resolveArtifactValue(reference, snapshot) {
|
|
88
|
+
const artifact = snapshotEntry(snapshot, reference.path);
|
|
89
|
+
if (artifact === undefined) {
|
|
90
|
+
return fail(reference, "artifact-missing", `no artifact at ${reference.path}`);
|
|
91
|
+
}
|
|
92
|
+
if (artifact.hash !== reference.hash) {
|
|
93
|
+
return fail(reference, "hash-mismatch", `expected ${reference.hash} but the artifact hash is ${artifact.hash}`);
|
|
94
|
+
}
|
|
95
|
+
// A caller supplies the rows of a fixture. A fixture that supplies rows for a figure would resolve a
|
|
96
|
+
// cell that no real artifact holds. Thus the refusal comes before the read of the rows.
|
|
97
|
+
const valueFileType = artifact.fileType;
|
|
98
|
+
if (fileTypeHoldsNoCell(valueFileType)) {
|
|
99
|
+
return fail(reference, "unreadable-artifact", `the ${valueFileType} at ${reference.path} holds no cell to read`);
|
|
100
|
+
}
|
|
101
|
+
// An artifact with no rows is pinned whole, thus it addresses no cell and every locator over it lands
|
|
102
|
+
// out of range.
|
|
103
|
+
const rows = artifact.rows ?? [];
|
|
104
|
+
const locator = reference.locator;
|
|
105
|
+
let selectedRow;
|
|
106
|
+
if (locator.row !== undefined) {
|
|
107
|
+
if (locator.row < 0 || locator.row >= rows.length) {
|
|
108
|
+
return fail(reference, "locator-out-of-range", `row ${locator.row} is outside the ${rows.length} rows`);
|
|
109
|
+
}
|
|
110
|
+
selectedRow = rows[locator.row];
|
|
111
|
+
}
|
|
112
|
+
else if (locator.rowFilter !== undefined) {
|
|
113
|
+
const filter = locator.rowFilter;
|
|
114
|
+
const matches = rows.filter((row) => row[filter.column] === filter.value);
|
|
115
|
+
if (matches.length === 0) {
|
|
116
|
+
return fail(reference, "locator-out-of-range", `no row where ${filter.column} equals ${String(filter.value)}`);
|
|
117
|
+
}
|
|
118
|
+
if (matches.length > 1) {
|
|
119
|
+
return fail(reference, "ambiguous-match", `${matches.length} rows where ${filter.column} equals ${String(filter.value)}`);
|
|
120
|
+
}
|
|
121
|
+
selectedRow = matches[0];
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// The locator schema forbids a locator with neither selector. The resolver cannot depend on a
|
|
125
|
+
// prior parse, thus it treats a row that no selector addresses as out of range.
|
|
126
|
+
return fail(reference, "locator-out-of-range", "the locator selects no row");
|
|
127
|
+
}
|
|
128
|
+
// A real table holds an empty cell, and a parser gives it back as an absent key, `undefined`, or
|
|
129
|
+
// `null`. None of the three is a value that a scalar reference can bind. An empty string is a value,
|
|
130
|
+
// thus it stays valid. The wider annotation admits the three, because the row type promises a value
|
|
131
|
+
// for each key that it holds.
|
|
132
|
+
const cell = selectedRow[locator.column];
|
|
133
|
+
if (cell === undefined || cell === null) {
|
|
134
|
+
return fail(reference, "locator-out-of-range", `column ${locator.column} holds no value in the selected row`);
|
|
135
|
+
}
|
|
136
|
+
const valueFailure = checkValueAssertion(reference, reference.assert?.value, reference.assert?.tolerance, cell);
|
|
137
|
+
return valueFailure !== undefined ? err(valueFailure) : ok({ type: "scalar", value: cell });
|
|
138
|
+
}
|
|
139
|
+
function resolveArtifactTable(reference, snapshot) {
|
|
140
|
+
const artifact = snapshotEntry(snapshot, reference.path);
|
|
141
|
+
if (artifact === undefined) {
|
|
142
|
+
return fail(reference, "artifact-missing", `no artifact at ${reference.path}`);
|
|
143
|
+
}
|
|
144
|
+
if (artifact.hash !== reference.hash) {
|
|
145
|
+
return fail(reference, "hash-mismatch", `expected ${reference.hash} but the artifact hash is ${artifact.hash}`);
|
|
146
|
+
}
|
|
147
|
+
const tableFileType = artifact.fileType;
|
|
148
|
+
if (fileTypeHoldsNoCell(tableFileType)) {
|
|
149
|
+
return fail(reference, "unreadable-artifact", `the ${tableFileType} at ${reference.path} holds no cell to read`);
|
|
150
|
+
}
|
|
151
|
+
const rows = artifact.rows ?? [];
|
|
152
|
+
const columns = reference.columns;
|
|
153
|
+
if (columns === undefined) {
|
|
154
|
+
return ok({ type: "table", rows });
|
|
155
|
+
}
|
|
156
|
+
// A name that no row holds addresses nothing. Without this check a projection onto an invented column
|
|
157
|
+
// gives an empty cell for each row and still resolves, which is the fabrication that grounding exists
|
|
158
|
+
// to reject.
|
|
159
|
+
const absent = columnsHeldByNoRow(rows, columns);
|
|
160
|
+
if (absent.length > 0) {
|
|
161
|
+
return fail(reference, "locator-out-of-range", `the table at ${reference.path} holds no column ${absent.join(", ")}`);
|
|
162
|
+
}
|
|
163
|
+
// Project each row onto the requested columns. A table tolerates a ragged row, thus a column that a
|
|
164
|
+
// given row lacks is left out of that row and is not a failure.
|
|
165
|
+
const projectedRows = rows.map((row) => {
|
|
166
|
+
const projected = {};
|
|
167
|
+
for (const column of columns) {
|
|
168
|
+
if (column in row) {
|
|
169
|
+
projected[column] = row[column];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return projected;
|
|
173
|
+
});
|
|
174
|
+
return ok({ type: "table", rows: projectedRows, columns });
|
|
175
|
+
}
|
|
176
|
+
function resolveArtifactFile(reference, snapshot) {
|
|
177
|
+
const artifact = snapshotEntry(snapshot, reference.path);
|
|
178
|
+
if (artifact === undefined) {
|
|
179
|
+
return fail(reference, "artifact-missing", `no artifact at ${reference.path}`);
|
|
180
|
+
}
|
|
181
|
+
if (artifact.hash !== reference.hash) {
|
|
182
|
+
return fail(reference, "hash-mismatch", `expected ${reference.hash} but the artifact hash is ${artifact.hash}`);
|
|
183
|
+
}
|
|
184
|
+
return ok({ type: "file", path: reference.path, hash: artifact.hash });
|
|
185
|
+
}
|
|
186
|
+
async function resolveDerivation(reference, snapshot) {
|
|
187
|
+
const numbers = [];
|
|
188
|
+
for (const input of reference.inputs) {
|
|
189
|
+
const inputResult = await resolve(input, snapshot);
|
|
190
|
+
if (inputResult.isErr()) {
|
|
191
|
+
// Keep the inner reason so a reviewer sees the real cause, for example a missing artifact under
|
|
192
|
+
// the derivation. The derivation itself did not fail on its own terms.
|
|
193
|
+
return fail(reference, inputResult.error.reason, inputResult.error.detail);
|
|
194
|
+
}
|
|
195
|
+
const resolved = inputResult.value;
|
|
196
|
+
const numeric = resolved.type === "scalar" ? asFiniteNumber(resolved.value) : undefined;
|
|
197
|
+
if (numeric === undefined) {
|
|
198
|
+
// The arithmetic needs a finite number. A cell that holds text does not address a usable
|
|
199
|
+
// scalar, thus the closest reason is that the coordinate is out of range.
|
|
200
|
+
return fail(reference, "locator-out-of-range", `input ${describeReference(input)} did not resolve to a finite number`);
|
|
201
|
+
}
|
|
202
|
+
numbers.push(numeric);
|
|
203
|
+
}
|
|
204
|
+
const a = numbers[0];
|
|
205
|
+
const b = numbers[1];
|
|
206
|
+
let result;
|
|
207
|
+
switch (reference.op) {
|
|
208
|
+
case "ratio":
|
|
209
|
+
result = a / b;
|
|
210
|
+
break;
|
|
211
|
+
case "delta":
|
|
212
|
+
result = a - b;
|
|
213
|
+
break;
|
|
214
|
+
case "pctChange":
|
|
215
|
+
// A fraction, not a percent. A change of one half gives 0.5, thus an author asserts 0.5 and a
|
|
216
|
+
// `unit` of `%` only tells a renderer how to show it.
|
|
217
|
+
result = (a - b) / b;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
if (!Number.isFinite(result)) {
|
|
221
|
+
return fail(reference, "locator-out-of-range", `operation ${reference.op} does not yield a finite value, because the divisor is zero`);
|
|
222
|
+
}
|
|
223
|
+
const valueFailure = checkValueAssertion(reference, reference.assert?.value, reference.assert?.tolerance, result);
|
|
224
|
+
return valueFailure !== undefined ? err(valueFailure) : ok({ type: "scalar", value: result });
|
|
225
|
+
}
|
|
226
|
+
function resolveCitation(reference, snapshot) {
|
|
227
|
+
const key = `${reference.idKind}:${reference.id}`;
|
|
228
|
+
if (snapshot.citations === undefined || !snapshot.citations.includes(key)) {
|
|
229
|
+
return fail(reference, "artifact-missing", `the citation ${key} is not in the pinned evidence`);
|
|
230
|
+
}
|
|
231
|
+
const value = { type: "citation", id: key };
|
|
232
|
+
const citationFailure = checkCitationAssertion(reference, reference.assert?.value, key);
|
|
233
|
+
return citationFailure !== undefined ? err(citationFailure) : ok(value);
|
|
234
|
+
}
|
|
235
|
+
async function resolve(reference, snapshot) {
|
|
236
|
+
switch (reference.kind) {
|
|
237
|
+
case "artifact-value":
|
|
238
|
+
return resolveArtifactValue(reference, snapshot);
|
|
239
|
+
case "artifact-table":
|
|
240
|
+
return resolveArtifactTable(reference, snapshot);
|
|
241
|
+
case "artifact-file":
|
|
242
|
+
return resolveArtifactFile(reference, snapshot);
|
|
243
|
+
case "derivation":
|
|
244
|
+
return resolveDerivation(reference, snapshot);
|
|
245
|
+
case "citation":
|
|
246
|
+
return resolveCitation(reference, snapshot);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** Make a resolver that reads only the snapshot. It holds no state, thus one instance serves every call. */
|
|
250
|
+
export function createFixtureResolver() {
|
|
251
|
+
return { resolve };
|
|
252
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pin of a report snapshot, which is the membership boundary of one report session.
|
|
3
|
+
*
|
|
4
|
+
* A report session freezes at one moment, and the analysis continues after that anchor. The snapshot
|
|
5
|
+
* states which artifacts existed at the anchor, and that is a question that no other record answers.
|
|
6
|
+
* Thus a session never cites an artifact that a later run produced.
|
|
7
|
+
*
|
|
8
|
+
* The artifact ledger holds one row for each path, and it keeps no history. Thus the set at a past
|
|
9
|
+
* moment is not recoverable later, and the pin must run at the anchor itself.
|
|
10
|
+
*/
|
|
11
|
+
import { type Result } from "neverthrow";
|
|
12
|
+
import type { Querier } from "../state/db.js";
|
|
13
|
+
import type { ReportSnapshot } from "./reference-resolver.js";
|
|
14
|
+
/**
|
|
15
|
+
* The reason that the pin gave no snapshot. The read of the ledger is the one operation that can
|
|
16
|
+
* fail. An analysis with no registered artifact is a normal answer, thus absence is not a member of
|
|
17
|
+
* this set. `cause` carries the underlying fault for a log.
|
|
18
|
+
*/
|
|
19
|
+
export type PinSnapshotError = {
|
|
20
|
+
kind: "ledger-read-failed";
|
|
21
|
+
cause: unknown;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Pin the snapshot of one analysis from the artifact ledger.
|
|
25
|
+
*
|
|
26
|
+
* The pin copies no cell and no byte. An entry pins identity alone, thus the snapshot grows with the
|
|
27
|
+
* count of artifacts and never with the size of the data. A read of an artifact belongs to the value
|
|
28
|
+
* tier, which runs one time for each report version.
|
|
29
|
+
*/
|
|
30
|
+
export declare function pinReportSnapshot(pool: Querier, analysisId: string): Promise<Result<ReportSnapshot, PinSnapshotError>>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pin of a report snapshot, which is the membership boundary of one report session.
|
|
3
|
+
*
|
|
4
|
+
* A report session freezes at one moment, and the analysis continues after that anchor. The snapshot
|
|
5
|
+
* states which artifacts existed at the anchor, and that is a question that no other record answers.
|
|
6
|
+
* Thus a session never cites an artifact that a later run produced.
|
|
7
|
+
*
|
|
8
|
+
* The artifact ledger holds one row for each path, and it keeps no history. Thus the set at a past
|
|
9
|
+
* moment is not recoverable later, and the pin must run at the anchor itself.
|
|
10
|
+
*/
|
|
11
|
+
import { err, ok } from "neverthrow";
|
|
12
|
+
import { queryAnalysisArtifacts } from "../state/artifacts.js";
|
|
13
|
+
/**
|
|
14
|
+
* Pin the snapshot of one analysis from the artifact ledger.
|
|
15
|
+
*
|
|
16
|
+
* The pin copies no cell and no byte. An entry pins identity alone, thus the snapshot grows with the
|
|
17
|
+
* count of artifacts and never with the size of the data. A read of an artifact belongs to the value
|
|
18
|
+
* tier, which runs one time for each report version.
|
|
19
|
+
*/
|
|
20
|
+
export async function pinReportSnapshot(pool, analysisId) {
|
|
21
|
+
let ledgerRows;
|
|
22
|
+
try {
|
|
23
|
+
ledgerRows = await queryAnalysisArtifacts(pool, analysisId);
|
|
24
|
+
}
|
|
25
|
+
catch (cause) {
|
|
26
|
+
// The query speaks the throw protocol of the `pg` driver. This is the thin wrapper that turns
|
|
27
|
+
// that throw into a value, thus each caller above reads a failure as data.
|
|
28
|
+
return err({ kind: "ledger-read-failed", cause });
|
|
29
|
+
}
|
|
30
|
+
// The ledger accepts any path. A null-prototype map keeps a key such as `__proto__` an ordinary
|
|
31
|
+
// entry, thus no path collides with a prototype slot.
|
|
32
|
+
const artifacts = Object.create(null);
|
|
33
|
+
for (const row of ledgerRows) {
|
|
34
|
+
artifacts[row.path] = { hash: row.hash, fileType: row.fileType };
|
|
35
|
+
}
|
|
36
|
+
// The pin fills no citation. No store holds the citation ids of an analysis, and validation
|
|
37
|
+
// matches a citation against the external authorities and never against a pinned list. Thus such
|
|
38
|
+
// a list would state nothing that a later read consumes.
|
|
39
|
+
return ok({ artifacts });
|
|
40
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seam that turns a report reference into a concrete value, and the value model that it gives back.
|
|
3
|
+
*
|
|
4
|
+
* A reference is authored belief. A resolver reads the pinned evidence and gives the value that the
|
|
5
|
+
* belief points at. The harness declares the seam here, thus a production realization can read storage,
|
|
6
|
+
* and a fixture realization can read an in-memory snapshot. The two obey the same contract.
|
|
7
|
+
*/
|
|
8
|
+
import type { Result } from "neverthrow";
|
|
9
|
+
import type { Reference, UnresolvedReference } from "../contracts/report-reference.js";
|
|
10
|
+
/**
|
|
11
|
+
* One pinned artifact: its content hash, and its rows as plain cells.
|
|
12
|
+
*
|
|
13
|
+
* A cell is a string or a number, because a realization gives back whatever its store holds. A
|
|
14
|
+
* text-backed artifact such as a CSV gives a numeric column as a string. Thus a realization is free to
|
|
15
|
+
* skip a type inference pass, and resolution compares a numeral by its numeric value and never by its
|
|
16
|
+
* JavaScript type.
|
|
17
|
+
*
|
|
18
|
+
* `rows` is optional because an image is pinned by its hash alone and holds no cells. An empty array in
|
|
19
|
+
* that position would say that the artifact holds zero rows, which is a different and false claim. The
|
|
20
|
+
* pin never populates `rows`. `rows` serves the fixture realization only, and that realization resolves
|
|
21
|
+
* from the snapshot itself. A production snapshot pins identity, and a value comes from a read of the
|
|
22
|
+
* artifact.
|
|
23
|
+
*
|
|
24
|
+
* `fileType` states a role, and it does not state a data format. `inferArtifactType` in
|
|
25
|
+
* `src/schemas/artifact-manifest.ts` gives each value that the ledger holds, and the values are
|
|
26
|
+
* `figure`, `script`, `log`, `notebook`, and `output`. Thus the field can refuse a kind of reference, and
|
|
27
|
+
* it can never confirm one. An `output` covers a table and an image alike.
|
|
28
|
+
*/
|
|
29
|
+
export interface ArtifactSnapshot {
|
|
30
|
+
hash: string;
|
|
31
|
+
fileType?: string | null;
|
|
32
|
+
rows?: Array<Record<string, string | number>>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The pinned evidence that a resolver reads. The `artifacts` map is keyed by the analysis-relative path,
|
|
36
|
+
* the same path that an artifact reference names and the same key as `cortex_artifacts.path`. That path
|
|
37
|
+
* is unique across the analysis, thus one map holds the artifacts of every run without a collision.
|
|
38
|
+
* `citations` holds each known external id as an `idKind:id` string, for example `pmid:12345`.
|
|
39
|
+
*/
|
|
40
|
+
export interface ReportSnapshot {
|
|
41
|
+
artifacts: Record<string, ArtifactSnapshot>;
|
|
42
|
+
citations?: string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The concrete value that a reference resolves to. A scalar comes from one artifact cell or one
|
|
46
|
+
* derivation. A table comes from a whole-table artifact. A file echoes the pin of a whole-file artifact,
|
|
47
|
+
* for example an image. A citation echo confirms that an external id is in the pinned evidence.
|
|
48
|
+
*/
|
|
49
|
+
export type ResolvedValue = {
|
|
50
|
+
type: "scalar";
|
|
51
|
+
value: string | number;
|
|
52
|
+
} | {
|
|
53
|
+
type: "table";
|
|
54
|
+
rows: Array<Record<string, string | number>>;
|
|
55
|
+
columns?: string[];
|
|
56
|
+
} | {
|
|
57
|
+
type: "file";
|
|
58
|
+
path: string;
|
|
59
|
+
hash: string;
|
|
60
|
+
} | {
|
|
61
|
+
type: "citation";
|
|
62
|
+
id: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* The capability seam that resolves one reference against a snapshot.
|
|
66
|
+
*
|
|
67
|
+
* The method is async because a production realization reads storage. A synchronous signature would
|
|
68
|
+
* force a local realization onto every host, thus the seam stays async even when a realization is
|
|
69
|
+
* in-memory.
|
|
70
|
+
*
|
|
71
|
+
* The `Ok` channel carries the resolved value. The `Err` channel carries the `UnresolvedReference`,
|
|
72
|
+
* which names the reason and the detail that a reference did not bind. The validator collects each such
|
|
73
|
+
* `Err` into its report, thus a caller reads the reason as data and resolution never throws.
|
|
74
|
+
*/
|
|
75
|
+
export interface ReferenceResolver {
|
|
76
|
+
resolve(reference: Reference, snapshot: ReportSnapshot): Promise<Result<ResolvedValue, UnresolvedReference>>;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Give back each name in `columns` that no row holds, in the order of `columns`.
|
|
80
|
+
*
|
|
81
|
+
* A table tolerates a ragged row, thus a column that only some rows hold is present. A column that no
|
|
82
|
+
* row holds is a name that addresses nothing, and it is the difference between a sparse table and a
|
|
83
|
+
* column that does not exist.
|
|
84
|
+
*
|
|
85
|
+
* A table with no rows gives back nothing. There is no evidence to contradict any name, and an empty
|
|
86
|
+
* result table is a real scientific outcome that must not fail for the shape of its emptiness.
|
|
87
|
+
*/
|
|
88
|
+
export declare function columnsHeldByNoRow(rows: Array<Record<string, string | number>>, columns: readonly string[]): string[];
|
|
89
|
+
/**
|
|
90
|
+
* Give back `true` for a file type that holds no cell for a reference to address.
|
|
91
|
+
*
|
|
92
|
+
* The file type states a role, and it does not state a data format. Thus the rule runs one way only. It
|
|
93
|
+
* refuses a kind, and it never confirms one. An `output` gives back `false`, because it covers a table and
|
|
94
|
+
* an image alike. Only a read of the artifact settles which one it is. An absent file type gives back
|
|
95
|
+
* `false` for the same reason.
|
|
96
|
+
*
|
|
97
|
+
* The structural tier and a resolver both read this one predicate. Thus the two tiers can never disagree
|
|
98
|
+
* about one reference.
|
|
99
|
+
*/
|
|
100
|
+
export declare function fileTypeHoldsNoCell(fileType: string | null | undefined): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Give back the snapshot entry at `path`, or `undefined` when the map holds no such own key.
|
|
103
|
+
*
|
|
104
|
+
* An agent authors a report reference, thus the path is untrusted text. The `artifacts` map can be a
|
|
105
|
+
* plain object: a fixture literal, or a `JSON.parse` of a stored snapshot. A bracket lookup on a plain
|
|
106
|
+
* object with a path such as `constructor` finds an inherited member. Then the refusal reads
|
|
107
|
+
* `hash-mismatch`, but the true answer is `artifact-missing`. The `Object.hasOwn` guard admits an own
|
|
108
|
+
* key only, thus an inherited member reads as absent.
|
|
109
|
+
*
|
|
110
|
+
* The structural tier and the fixture resolver both read this one lookup. Thus the two tiers can never
|
|
111
|
+
* disagree about membership.
|
|
112
|
+
*/
|
|
113
|
+
export declare function snapshotEntry(snapshot: ReportSnapshot, path: string): ArtifactSnapshot | undefined;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seam that turns a report reference into a concrete value, and the value model that it gives back.
|
|
3
|
+
*
|
|
4
|
+
* A reference is authored belief. A resolver reads the pinned evidence and gives the value that the
|
|
5
|
+
* belief points at. The harness declares the seam here, thus a production realization can read storage,
|
|
6
|
+
* and a fixture realization can read an in-memory snapshot. The two obey the same contract.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Give back each name in `columns` that no row holds, in the order of `columns`.
|
|
10
|
+
*
|
|
11
|
+
* A table tolerates a ragged row, thus a column that only some rows hold is present. A column that no
|
|
12
|
+
* row holds is a name that addresses nothing, and it is the difference between a sparse table and a
|
|
13
|
+
* column that does not exist.
|
|
14
|
+
*
|
|
15
|
+
* A table with no rows gives back nothing. There is no evidence to contradict any name, and an empty
|
|
16
|
+
* result table is a real scientific outcome that must not fail for the shape of its emptiness.
|
|
17
|
+
*/
|
|
18
|
+
export function columnsHeldByNoRow(rows, columns) {
|
|
19
|
+
if (rows.length === 0) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
return columns.filter((column) => !rows.some((row) => column in row));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The file type of an entry that holds no cell for a reference to address.
|
|
26
|
+
*
|
|
27
|
+
* The `satisfies` clause ties each literal to a member of `ArtifactType`. A renamed member of
|
|
28
|
+
* `ArtifactType` becomes a compile error here. A new member passes with no error, because the rule
|
|
29
|
+
* refuses a kind and never confirms one.
|
|
30
|
+
*/
|
|
31
|
+
const FILE_TYPES_WITH_NO_CELL = new Set(["figure", "script", "log", "notebook"]);
|
|
32
|
+
/**
|
|
33
|
+
* Give back `true` for a file type that holds no cell for a reference to address.
|
|
34
|
+
*
|
|
35
|
+
* The file type states a role, and it does not state a data format. Thus the rule runs one way only. It
|
|
36
|
+
* refuses a kind, and it never confirms one. An `output` gives back `false`, because it covers a table and
|
|
37
|
+
* an image alike. Only a read of the artifact settles which one it is. An absent file type gives back
|
|
38
|
+
* `false` for the same reason.
|
|
39
|
+
*
|
|
40
|
+
* The structural tier and a resolver both read this one predicate. Thus the two tiers can never disagree
|
|
41
|
+
* about one reference.
|
|
42
|
+
*/
|
|
43
|
+
export function fileTypeHoldsNoCell(fileType) {
|
|
44
|
+
return fileType !== undefined && fileType !== null && FILE_TYPES_WITH_NO_CELL.has(fileType);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Give back the snapshot entry at `path`, or `undefined` when the map holds no such own key.
|
|
48
|
+
*
|
|
49
|
+
* An agent authors a report reference, thus the path is untrusted text. The `artifacts` map can be a
|
|
50
|
+
* plain object: a fixture literal, or a `JSON.parse` of a stored snapshot. A bracket lookup on a plain
|
|
51
|
+
* object with a path such as `constructor` finds an inherited member. Then the refusal reads
|
|
52
|
+
* `hash-mismatch`, but the true answer is `artifact-missing`. The `Object.hasOwn` guard admits an own
|
|
53
|
+
* key only, thus an inherited member reads as absent.
|
|
54
|
+
*
|
|
55
|
+
* The structural tier and the fixture resolver both read this one lookup. Thus the two tiers can never
|
|
56
|
+
* disagree about membership.
|
|
57
|
+
*/
|
|
58
|
+
export function snapshotEntry(snapshot, path) {
|
|
59
|
+
return Object.hasOwn(snapshot.artifacts, path) ? snapshot.artifacts[path] : undefined;
|
|
60
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structural tier of reference resolution.
|
|
3
|
+
*
|
|
4
|
+
* The tier answers from the pinned snapshot alone, and it opens no file. Thus an authoring operation can
|
|
5
|
+
* run it on each change, and it stays instant while a document grows.
|
|
6
|
+
*
|
|
7
|
+
* The value tier gives the value that a reference points at, and only a read of the artifact gives that
|
|
8
|
+
* value. As a result the costly read happens one time for each report version, and never for each edit.
|
|
9
|
+
*/
|
|
10
|
+
import { type Result } from "neverthrow";
|
|
11
|
+
import type { Reference, UnresolvedReference } from "../contracts/report-reference.js";
|
|
12
|
+
import { type ReportSnapshot } from "./reference-resolver.js";
|
|
13
|
+
/**
|
|
14
|
+
* Validate one reference against the snapshot.
|
|
15
|
+
*
|
|
16
|
+
* The `Ok` channel carries no value. The tier answers membership, identity, and the file type, and a
|
|
17
|
+
* value comes from a read of the artifact. An `assert` is the authored belief about that value, thus this
|
|
18
|
+
* tier matches no assertion and a reference that carries one passes on its pin alone.
|
|
19
|
+
*
|
|
20
|
+
* The answer is synchronous, because the snapshot is already in memory.
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateReferenceStructure(reference: Reference, snapshot: ReportSnapshot): Result<void, UnresolvedReference>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structural tier of reference resolution.
|
|
3
|
+
*
|
|
4
|
+
* The tier answers from the pinned snapshot alone, and it opens no file. Thus an authoring operation can
|
|
5
|
+
* run it on each change, and it stays instant while a document grows.
|
|
6
|
+
*
|
|
7
|
+
* The value tier gives the value that a reference points at, and only a read of the artifact gives that
|
|
8
|
+
* value. As a result the costly read happens one time for each report version, and never for each edit.
|
|
9
|
+
*/
|
|
10
|
+
import { err, ok } from "neverthrow";
|
|
11
|
+
import { fileTypeHoldsNoCell, snapshotEntry } from "./reference-resolver.js";
|
|
12
|
+
/** Build an `Err` that carries the unresolved reference. The `detail` key is present only when there is a detail to carry. */
|
|
13
|
+
function fail(reference, reason, detail) {
|
|
14
|
+
const failure = detail !== undefined ? { reference, reason, detail } : { reference, reason };
|
|
15
|
+
return err(failure);
|
|
16
|
+
}
|
|
17
|
+
/** Validate the artifact pin of one reference: its membership in the snapshot, its identity, and the file type of its entry. */
|
|
18
|
+
function validatePin(reference, snapshot) {
|
|
19
|
+
const entry = snapshotEntry(snapshot, reference.path);
|
|
20
|
+
if (entry === undefined) {
|
|
21
|
+
return fail(reference, "artifact-missing", `no artifact at ${reference.path}`);
|
|
22
|
+
}
|
|
23
|
+
if (entry.hash !== reference.hash) {
|
|
24
|
+
return fail(reference, "hash-mismatch", `expected ${reference.hash} but the artifact hash is ${entry.hash}`);
|
|
25
|
+
}
|
|
26
|
+
// An `artifact-file` pins the bytes of a whole file, thus each file type is valid for it. A figure
|
|
27
|
+
// block binds through `artifact-file`, and a rule that refused a `figure` here would fail each figure
|
|
28
|
+
// of each report.
|
|
29
|
+
const readsACell = reference.kind === "artifact-value" || reference.kind === "artifact-table";
|
|
30
|
+
const fileType = entry.fileType;
|
|
31
|
+
if (readsACell && fileTypeHoldsNoCell(fileType)) {
|
|
32
|
+
return fail(reference, "unreadable-artifact", `the ${fileType} at ${reference.path} holds no cell to read`);
|
|
33
|
+
}
|
|
34
|
+
return ok();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Validate one reference against the snapshot.
|
|
38
|
+
*
|
|
39
|
+
* The `Ok` channel carries no value. The tier answers membership, identity, and the file type, and a
|
|
40
|
+
* value comes from a read of the artifact. An `assert` is the authored belief about that value, thus this
|
|
41
|
+
* tier matches no assertion and a reference that carries one passes on its pin alone.
|
|
42
|
+
*
|
|
43
|
+
* The answer is synchronous, because the snapshot is already in memory.
|
|
44
|
+
*/
|
|
45
|
+
export function validateReferenceStructure(reference, snapshot) {
|
|
46
|
+
switch (reference.kind) {
|
|
47
|
+
case "artifact-value":
|
|
48
|
+
case "artifact-table":
|
|
49
|
+
case "artifact-file":
|
|
50
|
+
return validatePin(reference, snapshot);
|
|
51
|
+
case "derivation": {
|
|
52
|
+
// A derivation holds no pin of its own, thus it is grounded exactly when both of its inputs
|
|
53
|
+
// are grounded. The failure keeps the reason of the input, because the derivation itself did
|
|
54
|
+
// not fail on its own terms.
|
|
55
|
+
for (const input of reference.inputs) {
|
|
56
|
+
const inputResult = validatePin(input, snapshot);
|
|
57
|
+
if (inputResult.isErr()) {
|
|
58
|
+
return fail(reference, inputResult.error.reason, inputResult.error.detail);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return ok();
|
|
62
|
+
}
|
|
63
|
+
case "citation":
|
|
64
|
+
// A citation holds an external id and no path, thus the snapshot has nothing to say about it.
|
|
65
|
+
// The citation authorities settle its truth at the value tier.
|
|
66
|
+
return ok();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mechanical validator of a report document.
|
|
3
|
+
*
|
|
4
|
+
* The schemas already carry the grammar: a strict object rejects a forbidden field, a binding is present
|
|
5
|
+
* by construction, and a metric value slot admits one scalar reference only. Thus a schema parse failure
|
|
6
|
+
* is the grammar rejection, and the validator does not re-implement it. The validator adds the three
|
|
7
|
+
* checks that a schema cannot make: it makes sure that each block id occurs one time only, it resolves
|
|
8
|
+
* each reference against the pinned evidence, and it warns about a free numeral in prose.
|
|
9
|
+
*/
|
|
10
|
+
import type { UnresolvedReference } from "../contracts/report-reference.js";
|
|
11
|
+
import { type AnyBlock, type CollectedReference, type ReportWarning } from "./block-walk.js";
|
|
12
|
+
import { type ReferenceResolver, type ReportSnapshot, type ResolvedValue } from "./reference-resolver.js";
|
|
13
|
+
export type { ReportWarning };
|
|
14
|
+
/**
|
|
15
|
+
* The cap on how many references resolve at the same time.
|
|
16
|
+
*
|
|
17
|
+
* A production resolver reads storage for each reference, and a large report holds hundreds of them. An
|
|
18
|
+
* unbounded fan-out would open one read for each reference at the same moment.
|
|
19
|
+
*/
|
|
20
|
+
export declare const RESOLUTION_CONCURRENCY = 8;
|
|
21
|
+
/** One schema conformance problem, reduced to a path and a message. */
|
|
22
|
+
export interface SchemaIssue {
|
|
23
|
+
path: string;
|
|
24
|
+
message: string;
|
|
25
|
+
}
|
|
26
|
+
/** One reference that did not resolve, tied to the block that carries it. */
|
|
27
|
+
export interface ResolutionFailure {
|
|
28
|
+
blockId: string;
|
|
29
|
+
failure: UnresolvedReference;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The result of validation. `valid` is false when the schema failed, a block id repeats, or a reference
|
|
33
|
+
* did not resolve. `duplicateIds` names each repeated id one time, in sorted order. The warnings ride
|
|
34
|
+
* along in either case.
|
|
35
|
+
*/
|
|
36
|
+
export type ReportValidation = {
|
|
37
|
+
valid: true;
|
|
38
|
+
warnings: ReportWarning[];
|
|
39
|
+
} | {
|
|
40
|
+
valid: false;
|
|
41
|
+
schemaIssues?: SchemaIssue[];
|
|
42
|
+
duplicateIds?: string[];
|
|
43
|
+
resolutionFailures?: ResolutionFailure[];
|
|
44
|
+
warnings: ReportWarning[];
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Match each column that a chart encoding names against the table that its binding resolved to.
|
|
48
|
+
*
|
|
49
|
+
* The binding schema admits a table reference only, thus a resolved value of another type means that the
|
|
50
|
+
* bound resolver broke its own contract. That is reported and never ignored, because a chart with no
|
|
51
|
+
* table behind it renders nothing and a silent skip would pass it as grounded.
|
|
52
|
+
*/
|
|
53
|
+
export declare function checkChartEncoding(entry: CollectedReference, value: ResolvedValue): UnresolvedReference | undefined;
|
|
54
|
+
/** The resolved values and the failures of one resolution pass over the references of a block tree. */
|
|
55
|
+
export interface ReferenceResolution {
|
|
56
|
+
/** The resolved value of each block whose reference resolved, keyed by its block id. */
|
|
57
|
+
resolvedByBlock: Map<string, ResolvedValue>;
|
|
58
|
+
/** Each reference that did not resolve, or whose chart encoding named an absent column. */
|
|
59
|
+
failures: ResolutionFailure[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve each reference of a block tree under the concurrency bound, and run the chart-encoding match.
|
|
63
|
+
*
|
|
64
|
+
* The walk collects each reference one time, `allWithConcurrency` bounds the fan-out, and
|
|
65
|
+
* `checkChartEncoding` catches a chart that plots a column which the bound table does not hold. The record
|
|
66
|
+
* gate and the preview share this one pass, thus the two refuse the same references. A value-bearing block
|
|
67
|
+
* whose reference resolved carries its value in the map. A claim or a citation reference reads no value
|
|
68
|
+
* from the map. The walk collects every failure, thus a reviewer sees each block that broke.
|
|
69
|
+
*/
|
|
70
|
+
export declare function resolveDocumentReferences(sections: readonly AnyBlock[], snapshot: ReportSnapshot, resolver: ReferenceResolver): Promise<ReferenceResolution>;
|
|
71
|
+
/** Resolve each reference, collect the schema conformance and the resolution outcomes, and warn on prose. */
|
|
72
|
+
export declare function validateReport(document: unknown, snapshot: ReportSnapshot, resolver: ReferenceResolver): Promise<ReportValidation>;
|