@inflexa-ai/harness 0.16.0 → 0.18.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.
Files changed (295) hide show
  1. package/dist/agents/conversation-agent.d.ts +3 -0
  2. package/dist/agents/conversation-agent.js +5 -2
  3. package/dist/agents/report-builder.d.ts +2 -0
  4. package/dist/agents/report-builder.js +2 -0
  5. package/dist/agents/sandbox/shared.d.ts +3 -0
  6. package/dist/agents/sandbox/shared.js +5 -0
  7. package/dist/agents/sandbox/types.d.ts +1 -1
  8. package/dist/app/chat-turn.d.ts +2 -0
  9. package/dist/app/chat-turn.js +11 -2
  10. package/dist/app/spawn-report-session.d.ts +85 -0
  11. package/dist/app/spawn-report-session.js +83 -0
  12. package/dist/app/synthesize-run.d.ts +3 -0
  13. package/dist/app/synthesize-run.js +2 -1
  14. package/dist/citations/__fixtures__/resolver.d.ts +3 -0
  15. package/dist/citations/__fixtures__/resolver.js +9 -0
  16. package/dist/citations/aggregate.d.ts +3 -0
  17. package/dist/citations/aggregate.js +44 -0
  18. package/dist/citations/clients/arxiv.d.ts +6 -0
  19. package/dist/citations/clients/arxiv.js +34 -0
  20. package/dist/citations/clients/common.d.ts +11 -0
  21. package/dist/citations/clients/common.js +43 -0
  22. package/dist/citations/clients/crossref.d.ts +8 -0
  23. package/dist/citations/clients/crossref.js +98 -0
  24. package/dist/citations/clients/doi-registry.d.ts +3 -0
  25. package/dist/citations/clients/doi-registry.js +95 -0
  26. package/dist/citations/clients/pubmed.d.ts +6 -0
  27. package/dist/citations/clients/pubmed.js +83 -0
  28. package/dist/citations/clients/semantic-scholar.d.ts +6 -0
  29. package/dist/citations/clients/semantic-scholar.js +60 -0
  30. package/dist/citations/compare.d.ts +9 -0
  31. package/dist/citations/compare.js +72 -0
  32. package/dist/citations/match.d.ts +11 -0
  33. package/dist/citations/match.js +173 -0
  34. package/dist/citations/normalize.d.ts +31 -0
  35. package/dist/citations/normalize.js +187 -0
  36. package/dist/citations/plan.d.ts +2 -0
  37. package/dist/citations/plan.js +59 -0
  38. package/dist/citations/resolve.d.ts +28 -0
  39. package/dist/citations/resolve.js +238 -0
  40. package/dist/citations/types.d.ts +583 -0
  41. package/dist/citations/types.js +129 -0
  42. package/dist/contracts/chat-events.d.ts +9 -0
  43. package/dist/contracts/index.d.ts +6 -2
  44. package/dist/contracts/index.js +3 -1
  45. package/dist/contracts/organ-system.d.ts +49 -0
  46. package/dist/contracts/organ-system.js +67 -0
  47. package/dist/contracts/report-blocks.d.ts +564 -0
  48. package/dist/contracts/report-blocks.js +117 -0
  49. package/dist/contracts/report-reference.d.ts +459 -0
  50. package/dist/contracts/report-reference.js +256 -0
  51. package/dist/contracts/schemas/chat-events.d.ts +2 -0
  52. package/dist/contracts/schemas/chat-events.js +2 -0
  53. package/dist/contracts/schemas/chat-parts.d.ts +8 -8
  54. package/dist/contracts/severity.d.ts +15 -0
  55. package/dist/contracts/severity.js +10 -0
  56. package/dist/contracts/target-dossier.d.ts +8673 -12970
  57. package/dist/contracts/target-dossier.js +534 -591
  58. package/dist/data/safety-panel-schema.d.ts +18 -5
  59. package/dist/data/safety-panel-schema.js +4 -4
  60. package/dist/execution/artifact-metadata.js +1 -0
  61. package/dist/execution/report-runner.js +1 -3
  62. package/dist/execution/run-synthesis.d.ts +3 -0
  63. package/dist/execution/run-synthesis.js +3 -0
  64. package/dist/index.d.ts +12 -1
  65. package/dist/index.js +22 -0
  66. package/dist/lib/async-utils.d.ts +16 -4
  67. package/dist/lib/async-utils.js +20 -15
  68. package/dist/lib/cache.d.ts +8 -0
  69. package/dist/lib/cache.js +35 -0
  70. package/dist/lib/host-concurrency.js +1 -0
  71. package/dist/lib/rate-limit.d.ts +35 -0
  72. package/dist/lib/rate-limit.js +121 -0
  73. package/dist/literature/sources/arxiv.d.ts +22 -0
  74. package/dist/literature/sources/arxiv.js +103 -0
  75. package/dist/literature/sources/http.d.ts +28 -0
  76. package/dist/literature/sources/http.js +97 -0
  77. package/dist/literature/sources/pubmed.d.ts +68 -0
  78. package/dist/literature/sources/pubmed.js +266 -0
  79. package/dist/literature/sources/semantic-scholar.d.ts +24 -0
  80. package/dist/literature/sources/semantic-scholar.js +71 -0
  81. package/dist/loop/__fixtures__/scripted-provider.js +23 -2
  82. package/dist/loop/run-agent.js +58 -10
  83. package/dist/loop/tool-detail.d.ts +40 -0
  84. package/dist/loop/tool-detail.js +35 -3
  85. package/dist/loop/types.d.ts +13 -0
  86. package/dist/memory/thread-history.d.ts +10 -0
  87. package/dist/memory/thread-history.js +12 -1
  88. package/dist/memory/thread-store.d.ts +142 -25
  89. package/dist/memory/thread-store.js +228 -40
  90. package/dist/prompts/literature-reviewer.d.ts +1 -1
  91. package/dist/prompts/literature-reviewer.js +2 -0
  92. package/dist/prompts/target-assessment/briefs/executive-recommendation.d.ts +1 -1
  93. package/dist/prompts/target-assessment/briefs/executive-recommendation.js +5 -5
  94. package/dist/prompts/target-assessment/briefs/liability-bullets.d.ts +1 -1
  95. package/dist/prompts/target-assessment/briefs/liability-bullets.js +1 -1
  96. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.d.ts +1 -1
  97. package/dist/prompts/target-assessment/briefs/target-organ-liabilities.js +11 -3
  98. package/dist/prompts/target-assessment/investigation/adversarial-critique.d.ts +1 -0
  99. package/dist/prompts/target-assessment/investigation/adversarial-critique.js +67 -0
  100. package/dist/prompts/target-assessment/investigation/claim-reverification.d.ts +1 -0
  101. package/dist/prompts/target-assessment/investigation/claim-reverification.js +49 -0
  102. package/dist/prompts/target-assessment/investigation/index.d.ts +3 -0
  103. package/dist/prompts/target-assessment/investigation/index.js +3 -0
  104. package/dist/prompts/target-assessment/investigation/mechanism-proposal.d.ts +1 -0
  105. package/dist/prompts/target-assessment/investigation/mechanism-proposal.js +49 -0
  106. package/dist/prompts/target-assessment/tox-voice/index.d.ts +1 -1
  107. package/dist/prompts/target-assessment/tox-voice/vocabulary.d.ts +8 -5
  108. package/dist/prompts/target-assessment/tox-voice/vocabulary.js +9 -20
  109. package/dist/providers/__fixtures__/session.d.ts +5 -4
  110. package/dist/providers/__fixtures__/session.js +4 -4
  111. package/dist/report-model/block-walk.d.ts +56 -0
  112. package/dist/report-model/block-walk.js +89 -0
  113. package/dist/report-model/draft-finish.d.ts +60 -0
  114. package/dist/report-model/draft-finish.js +56 -0
  115. package/dist/report-model/draft-operations.d.ts +125 -0
  116. package/dist/report-model/draft-operations.js +341 -0
  117. package/dist/report-model/draft-read.d.ts +59 -0
  118. package/dist/report-model/draft-read.js +109 -0
  119. package/dist/report-model/draft.d.ts +51 -0
  120. package/dist/report-model/draft.js +32 -0
  121. package/dist/report-model/fixture-resolver.d.ts +8 -0
  122. package/dist/report-model/fixture-resolver.js +252 -0
  123. package/dist/report-model/mint-snapshot.d.ts +30 -0
  124. package/dist/report-model/mint-snapshot.js +40 -0
  125. package/dist/report-model/reference-resolver.d.ts +113 -0
  126. package/dist/report-model/reference-resolver.js +60 -0
  127. package/dist/report-model/structural-validation.d.ts +22 -0
  128. package/dist/report-model/structural-validation.js +68 -0
  129. package/dist/report-model/validate.d.ts +40 -0
  130. package/dist/report-model/validate.js +80 -0
  131. package/dist/report-render/chart.d.ts +33 -0
  132. package/dist/report-render/chart.js +538 -0
  133. package/dist/report-render/page.d.ts +127 -0
  134. package/dist/report-render/page.js +307 -0
  135. package/dist/report-render/references.d.ts +32 -0
  136. package/dist/report-render/references.js +44 -0
  137. package/dist/report-render/render.d.ts +25 -0
  138. package/dist/report-render/render.js +125 -0
  139. package/dist/report-render/script-json.d.ts +13 -0
  140. package/dist/report-render/script-json.js +15 -0
  141. package/dist/report-render/types.d.ts +39 -0
  142. package/dist/report-render/types.js +10 -0
  143. package/dist/report-render/views/chart-view.d.ts +11 -0
  144. package/dist/report-render/views/chart-view.js +23 -0
  145. package/dist/report-render/views/page-view.d.ts +20 -0
  146. package/dist/report-render/views/page-view.js +49 -0
  147. package/dist/report-render/views/prose.d.ts +28 -0
  148. package/dist/report-render/views/prose.js +75 -0
  149. package/dist/report-render/views/references-view.d.ts +20 -0
  150. package/dist/report-render/views/references-view.js +64 -0
  151. package/dist/report-render/views/values.d.ts +42 -0
  152. package/dist/report-render/views/values.js +43 -0
  153. package/dist/runtime/assemble.d.ts +50 -4
  154. package/dist/runtime/assemble.js +47 -3
  155. package/dist/runtime/boot.d.ts +3 -3
  156. package/dist/runtime/boot.js +22 -5
  157. package/dist/runtime/dbos.d.ts +4 -4
  158. package/dist/state/artifacts.d.ts +16 -0
  159. package/dist/state/artifacts.js +18 -0
  160. package/dist/state/init.js +77 -1
  161. package/dist/state/purge-analysis.js +1 -0
  162. package/dist/state/report-versions.d.ts +132 -0
  163. package/dist/state/report-versions.js +194 -0
  164. package/dist/state/schema.d.ts +2 -2
  165. package/dist/tasks/data-profile.js +1 -0
  166. package/dist/tools/bio/chembl.js +1 -0
  167. package/dist/tools/bio/comptox.js +1 -0
  168. package/dist/tools/bio/drug-gene-interactions.js +1 -0
  169. package/dist/tools/bio/gene-disease-evidence.js +1 -0
  170. package/dist/tools/bio/gene-preclinical-profile.d.ts +1 -1
  171. package/dist/tools/bio/gene-preclinical-profile.js +1 -0
  172. package/dist/tools/bio/keys.d.ts +4 -2
  173. package/dist/tools/bio/keys.js +3 -2
  174. package/dist/tools/bio/lookup-annotation.js +1 -0
  175. package/dist/tools/bio/opentargets.d.ts +3 -3
  176. package/dist/tools/bio/opentargets.js +4 -3
  177. package/dist/tools/bio/pubchem.js +1 -0
  178. package/dist/tools/bio/pubmed.js +4 -4
  179. package/dist/tools/bio/search-clinical-trials.js +1 -0
  180. package/dist/tools/bio/search-faers.js +1 -0
  181. package/dist/tools/bio/search-geo-datasets.js +1 -0
  182. package/dist/tools/bio/search-interactions.js +1 -0
  183. package/dist/tools/bio/target-safety.d.ts +4 -6
  184. package/dist/tools/bio/target-safety.js +7 -6
  185. package/dist/tools/define-tool.d.ts +19 -12
  186. package/dist/tools/define-tool.js +4 -2
  187. package/dist/tools/display/show-user.js +1 -0
  188. package/dist/tools/iterate-report.js +2 -0
  189. package/dist/tools/lib/chembl-client.d.ts +2 -0
  190. package/dist/tools/lib/chembl-client.js +1 -0
  191. package/dist/tools/lib/monarch-client.d.ts +42 -0
  192. package/dist/tools/lib/monarch-client.js +100 -0
  193. package/dist/tools/lib/ncbi-utils.d.ts +3 -58
  194. package/dist/tools/lib/ncbi-utils.js +2 -286
  195. package/dist/tools/lib/pubmed-client.d.ts +5 -12
  196. package/dist/tools/lib/pubmed-client.js +20 -79
  197. package/dist/tools/report/build-report.js +1 -0
  198. package/dist/tools/report/mint-preview-url.js +1 -0
  199. package/dist/tools/report/preview-snapshot.js +1 -0
  200. package/dist/tools/report/submit-report.js +1 -0
  201. package/dist/tools/report/version-fs.js +4 -0
  202. package/dist/tools/report-authoring/authoring-tools.d.ts +134 -0
  203. package/dist/tools/report-authoring/authoring-tools.js +352 -0
  204. package/dist/tools/research/context7-docs.js +2 -0
  205. package/dist/tools/research/generate-analogy-report.js +3 -2
  206. package/dist/tools/research/generate-plan.js +3 -0
  207. package/dist/tools/research/index.d.ts +1 -0
  208. package/dist/tools/research/index.js +1 -0
  209. package/dist/tools/research/inspect-data-profile.js +4 -0
  210. package/dist/tools/research/inspect-run.d.ts +1 -1
  211. package/dist/tools/research/literature-reviewer.d.ts +3 -0
  212. package/dist/tools/research/literature-reviewer.js +3 -0
  213. package/dist/tools/research/resolve-citation.d.ts +3 -0
  214. package/dist/tools/research/resolve-citation.js +15 -0
  215. package/dist/tools/research/search-arxiv.d.ts +3 -19
  216. package/dist/tools/research/search-arxiv.js +10 -105
  217. package/dist/tools/research/search-github-repos.js +1 -0
  218. package/dist/tools/research/search-semantic-scholar.d.ts +8 -33
  219. package/dist/tools/research/search-semantic-scholar.js +30 -88
  220. package/dist/tools/sandbox/list-available-packages.js +23 -0
  221. package/dist/tools/sandbox/list-available-refs.js +37 -2
  222. package/dist/tools/sandbox/report-blocker.js +1 -0
  223. package/dist/tools/sandbox/skills.js +2 -0
  224. package/dist/tools/workspace/file-stat.js +1 -0
  225. package/dist/tools/workspace/show-file.js +2 -0
  226. package/dist/tools/workspace/show-plan.js +1 -0
  227. package/dist/workflows/execute-analysis.d.ts +8 -1
  228. package/dist/workflows/execute-analysis.js +6 -5
  229. package/dist/workflows/execute-target-assessment.d.ts +13 -2
  230. package/dist/workflows/execute-target-assessment.js +78 -16
  231. package/dist/workflows/sandbox-step.d.ts +5 -0
  232. package/dist/workflows/sandbox-step.js +1 -0
  233. package/dist/workflows/target-assessment/assemblers/index.d.ts +1 -1
  234. package/dist/workflows/target-assessment/assemblers/index.js +1 -1
  235. package/dist/workflows/target-assessment/assemblers/literature.d.ts +9 -27
  236. package/dist/workflows/target-assessment/assemblers/literature.js +32 -48
  237. package/dist/workflows/target-assessment/assemblers/orchestrator.d.ts +17 -9
  238. package/dist/workflows/target-assessment/assemblers/orchestrator.js +195 -85
  239. package/dist/workflows/target-assessment/assemblers/safety.d.ts +70 -48
  240. package/dist/workflows/target-assessment/assemblers/safety.js +125 -42
  241. package/dist/workflows/target-assessment/collectors/index.d.ts +5 -2
  242. package/dist/workflows/target-assessment/collectors/index.js +48 -5
  243. package/dist/workflows/target-assessment/coverage.d.ts +43 -27
  244. package/dist/workflows/target-assessment/coverage.js +33 -29
  245. package/dist/workflows/target-assessment/fanout/index.js +1 -0
  246. package/dist/workflows/target-assessment/investigation/index.d.ts +97 -0
  247. package/dist/workflows/target-assessment/investigation/index.js +419 -0
  248. package/dist/workflows/target-assessment/lib/approval-precedents.d.ts +26 -5
  249. package/dist/workflows/target-assessment/lib/approval-precedents.js +95 -29
  250. package/dist/workflows/target-assessment/lib/citation-validator.js +1 -1
  251. package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +2 -2
  252. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.d.ts +2 -3
  253. package/dist/workflows/target-assessment/lib/clinical-evidence-attribution.js +0 -4
  254. package/dist/workflows/target-assessment/lib/compute-derived.d.ts +6 -5
  255. package/dist/workflows/target-assessment/lib/compute-derived.js +14 -5
  256. package/dist/workflows/target-assessment/lib/family-complex-supplement.d.ts +5 -6
  257. package/dist/workflows/target-assessment/lib/family-complex-supplement.js +2 -3
  258. package/dist/workflows/target-assessment/lib/fda-label-safety.d.ts +72 -0
  259. package/dist/workflows/target-assessment/lib/fda-label-safety.js +166 -0
  260. package/dist/workflows/target-assessment/lib/heterodimer-filter.d.ts +0 -9
  261. package/dist/workflows/target-assessment/lib/heterodimer-filter.js +0 -10
  262. package/dist/workflows/target-assessment/lib/hpo-organ-map.d.ts +34 -0
  263. package/dist/workflows/target-assessment/lib/hpo-organ-map.js +63 -0
  264. package/dist/workflows/target-assessment/lib/impc-organ-map.d.ts +19 -0
  265. package/dist/workflows/target-assessment/lib/impc-organ-map.js +39 -0
  266. package/dist/workflows/target-assessment/lib/meddra-organ-map.d.ts +24 -14
  267. package/dist/workflows/target-assessment/lib/meddra-organ-map.js +59 -47
  268. package/dist/workflows/target-assessment/lib/recommendation-sanitizer.js +1 -1
  269. package/dist/workflows/target-assessment/lib/regulatory-actions.d.ts +2 -3
  270. package/dist/workflows/target-assessment/lib/regulatory-actions.js +33 -4
  271. package/dist/workflows/target-assessment/lib/safety-corroboration.d.ts +44 -0
  272. package/dist/workflows/target-assessment/lib/safety-corroboration.js +273 -0
  273. package/dist/workflows/target-assessment/lib/target-identity-filter.d.ts +5 -7
  274. package/dist/workflows/target-assessment/lib/target-identity-filter.js +5 -7
  275. package/dist/workflows/target-assessment/metrics.d.ts +1 -6
  276. package/dist/workflows/target-assessment/metrics.js +1 -13
  277. package/dist/workflows/target-assessment/phase4-assemble.d.ts +2149 -809
  278. package/dist/workflows/target-assessment/phase4-assemble.js +19 -19
  279. package/dist/workflows/target-assessment/phase5-persist.d.ts +20 -21
  280. package/dist/workflows/target-assessment/phase5-persist.js +56 -173
  281. package/dist/workflows/target-assessment/schemas.d.ts +48 -3
  282. package/dist/workflows/target-assessment/schemas.js +19 -0
  283. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.d.ts +1 -0
  284. package/dist/workflows/target-assessment/steps/fanout/polypharm-for-one-modulator-step.js +2 -0
  285. package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +27 -63
  286. package/dist/workflows/target-assessment/steps/phase2-aggregate.js +0 -4
  287. package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +28 -155
  288. package/dist/workflows/target-assessment/steps/phase3-aggregate.js +0 -6
  289. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.d.ts +0 -6
  290. package/dist/workflows/target-assessment/steps/synthesis/diagnostics.js +0 -18
  291. package/dist/workflows/target-assessment/sub-workflows/per-modulator-polypharm.d.ts +1 -0
  292. package/dist/workflows/target-assessment/synthesis/index.d.ts +52 -5
  293. package/dist/workflows/target-assessment/synthesis/index.js +4 -1
  294. package/dist/workflows/target-assessment/synthesis-probes.d.ts +3 -3
  295. 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 mint 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 mint 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 mint 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 MintSnapshotError = {
20
+ kind: "ledger-read-failed";
21
+ cause: unknown;
22
+ };
23
+ /**
24
+ * Mint the snapshot of one analysis from the artifact ledger.
25
+ *
26
+ * The mint 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 mintReportSnapshot(pool: Querier, analysisId: string): Promise<Result<ReportSnapshot, MintSnapshotError>>;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The mint 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 mint must run at the anchor itself.
10
+ */
11
+ import { err, ok } from "neverthrow";
12
+ import { queryAnalysisArtifacts } from "../state/artifacts.js";
13
+ /**
14
+ * Mint the snapshot of one analysis from the artifact ledger.
15
+ *
16
+ * The mint 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 mintReportSnapshot(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 mint 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 minted 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
+ * mint 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,40 @@
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 ReportWarning } from "./block-walk.js";
12
+ import { type ReferenceResolver, type ReportSnapshot } from "./reference-resolver.js";
13
+ export type { ReportWarning };
14
+ /** One schema conformance problem, reduced to a path and a message. */
15
+ export interface SchemaIssue {
16
+ path: string;
17
+ message: string;
18
+ }
19
+ /** One reference that did not resolve, tied to the block that carries it. */
20
+ export interface ResolutionFailure {
21
+ blockId: string;
22
+ failure: UnresolvedReference;
23
+ }
24
+ /**
25
+ * The result of validation. `valid` is false when the schema failed, a block id repeats, or a reference
26
+ * did not resolve. `duplicateIds` names each repeated id one time, in sorted order. The warnings ride
27
+ * along in either case.
28
+ */
29
+ export type ReportValidation = {
30
+ valid: true;
31
+ warnings: ReportWarning[];
32
+ } | {
33
+ valid: false;
34
+ schemaIssues?: SchemaIssue[];
35
+ duplicateIds?: string[];
36
+ resolutionFailures?: ResolutionFailure[];
37
+ warnings: ReportWarning[];
38
+ };
39
+ /** Resolve each reference, collect the schema conformance and the resolution outcomes, and warn on prose. */
40
+ export declare function validateReport(document: unknown, snapshot: ReportSnapshot, resolver: ReferenceResolver): Promise<ReportValidation>;