@inflexa-ai/harness 0.18.0 → 0.19.1

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 (83) hide show
  1. package/dist/agents/report-session-agent.d.ts +72 -0
  2. package/dist/agents/report-session-agent.js +87 -0
  3. package/dist/app/report-session-runtime.d.ts +72 -0
  4. package/dist/app/report-session-runtime.js +183 -0
  5. package/dist/app/spawn-report-session.d.ts +32 -5
  6. package/dist/app/spawn-report-session.js +19 -0
  7. package/dist/config/environment-stores.d.ts +12 -7
  8. package/dist/config/resource-limits.d.ts +6 -10
  9. package/dist/config/resource-limits.js +6 -44
  10. package/dist/contracts/report-reference.d.ts +4 -0
  11. package/dist/contracts/report-reference.js +3 -0
  12. package/dist/index.d.ts +2 -1
  13. package/dist/lib/chrome.d.ts +14 -0
  14. package/dist/lib/chrome.js +7 -0
  15. package/dist/lib/page-capture.d.ts +47 -0
  16. package/dist/lib/page-capture.js +83 -0
  17. package/dist/loop/run-agent.js +104 -15
  18. package/dist/memory/ai-sdk-message-storage.d.ts +9 -4
  19. package/dist/memory/ai-sdk-message-storage.js +9 -4
  20. package/dist/memory/count-tokens.js +34 -5
  21. package/dist/memory/thread-store.d.ts +4 -4
  22. package/dist/memory/thread-store.js +18 -9
  23. package/dist/prompts/report-session.d.ts +1 -0
  24. package/dist/prompts/report-session.js +110 -0
  25. package/dist/providers/ai-sdk.d.ts +158 -3
  26. package/dist/providers/ai-sdk.js +234 -23
  27. package/dist/providers/errors.d.ts +14 -0
  28. package/dist/providers/errors.js +103 -18
  29. package/dist/providers/types.d.ts +33 -0
  30. package/dist/providers/types.js +10 -1
  31. package/dist/report-model/assert-rules.d.ts +44 -0
  32. package/dist/report-model/assert-rules.js +97 -0
  33. package/dist/report-model/draft-hash.d.ts +14 -0
  34. package/dist/report-model/draft-hash.js +36 -0
  35. package/dist/report-model/fixture-resolver.js +2 -71
  36. package/dist/report-model/{mint-snapshot.d.ts → pin-snapshot.d.ts} +7 -7
  37. package/dist/report-model/{mint-snapshot.js → pin-snapshot.js} +7 -7
  38. package/dist/report-model/production-resolver.d.ts +124 -0
  39. package/dist/report-model/production-resolver.js +801 -0
  40. package/dist/report-model/reference-resolver.d.ts +7 -1
  41. package/dist/report-model/validate.d.ts +34 -2
  42. package/dist/report-model/validate.js +37 -17
  43. package/dist/report-render/page.d.ts +23 -0
  44. package/dist/report-render/page.js +36 -3
  45. package/dist/report-render/value-bridge.d.ts +62 -0
  46. package/dist/report-render/value-bridge.js +82 -0
  47. package/dist/runtime/assemble.d.ts +39 -0
  48. package/dist/runtime/assemble.js +67 -6
  49. package/dist/sandbox/mount-plan.d.ts +7 -0
  50. package/dist/sandbox/mount-plan.js +7 -2
  51. package/dist/state/init.js +36 -0
  52. package/dist/state/purge-analysis.js +1 -0
  53. package/dist/state/report-session-state.d.ts +139 -0
  54. package/dist/state/report-session-state.js +177 -0
  55. package/dist/state/report-versions.d.ts +3 -6
  56. package/dist/state/report-versions.js +11 -38
  57. package/dist/state/snapshot-parse.d.ts +34 -0
  58. package/dist/state/snapshot-parse.js +82 -0
  59. package/dist/tasks/extract-values-script.d.ts +64 -0
  60. package/dist/tasks/extract-values-script.js +180 -0
  61. package/dist/tasks/extract-values.d.ts +124 -0
  62. package/dist/tasks/extract-values.js +193 -0
  63. package/dist/tools/ad-hoc-router.d.ts +7 -2
  64. package/dist/tools/ad-hoc-router.js +2 -1
  65. package/dist/tools/define-tool.d.ts +29 -0
  66. package/dist/tools/define-tool.js +26 -0
  67. package/dist/tools/report/preview-snapshot.d.ts +1 -1
  68. package/dist/tools/report/preview-snapshot.js +11 -50
  69. package/dist/tools/report-authoring/authoring-tools.d.ts +181 -24
  70. package/dist/tools/report-authoring/authoring-tools.js +152 -44
  71. package/dist/tools/report-session/examine-page.d.ts +90 -0
  72. package/dist/tools/report-session/examine-page.js +136 -0
  73. package/dist/tools/report-session/index.d.ts +10 -0
  74. package/dist/tools/report-session/index.js +10 -0
  75. package/dist/tools/report-session/preview-report.d.ts +105 -0
  76. package/dist/tools/report-session/preview-report.js +269 -0
  77. package/dist/tools/report-session/record-version.d.ts +102 -0
  78. package/dist/tools/report-session/record-version.js +139 -0
  79. package/dist/tools/research/generate-plan.js +4 -2
  80. package/dist/tools/sandbox/list-available-packages.js +2 -1
  81. package/dist/tools/sandbox/list-available-refs.d.ts +5 -1
  82. package/dist/tools/sandbox/list-available-refs.js +13 -6
  83. package/package.json +10 -8
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The assert rules of the value tier. A resolver reads a fresh value, and these functions match it
3
+ * against the authored belief.
4
+ *
5
+ * Each realization of the resolver calls these functions. Thus one semantics exists, and the fixture
6
+ * stays the executable specification of the value tier.
7
+ */
8
+ import type { Reference, UnresolvedReference } from "../contracts/report-reference.js";
9
+ /**
10
+ * Read an authored value or a resolved cell as a finite number.
11
+ *
12
+ * A text-backed artifact such as a CSV holds every cell as a string, thus a numeric column arrives as
13
+ * `"0.01"` and not as `0.01`. A comparison that respects the JavaScript type would fail an author who
14
+ * states the number, and the arithmetic of a derivation could never run over a real CSV.
15
+ *
16
+ * The parse reads the whole string, thus `"12 genes"` stays text and never reads as 12. `Number` accepts
17
+ * the exponent form that a p-value uses, for example `"1.2e-45"`. A blank string is not a number, because
18
+ * `Number("")` gives 0 and an empty cell must never read as zero.
19
+ */
20
+ export declare function asFiniteNumber(value: string | number): number | undefined;
21
+ /**
22
+ * Match a row-filter value against a resolved cell.
23
+ *
24
+ * A text-backed artifact such as a CSV holds every cell as a string, thus a filter value of `3` must
25
+ * select the cell `"3"`. The compare reads both sides as a finite number, and it selects the row where the
26
+ * two numbers are equal. A side that is not a finite number compares by exact value, thus a text key
27
+ * selects by its exact text. The match is exact and carries no tolerance, because a filter names a
28
+ * discrete key and not a computed value with float noise.
29
+ */
30
+ export declare function cellMatchesFilterValue(cell: string | number | null | undefined, filterValue: string | number): boolean;
31
+ /**
32
+ * Match a resolved value against an authored one.
33
+ *
34
+ * The comparison is numeric whenever both sides read as a finite number, thus the CSV cell `"0.01"`
35
+ * matches the number 0.01 that an author states. An authored tolerance is an absolute difference, and it
36
+ * is the author's own statement of how close is close enough. With no tolerance the comparison is
37
+ * relative, because an exact match would fail on the float noise of a computed value. Any other pair
38
+ * matches on exact equality.
39
+ */
40
+ export declare function valuesMatch(expected: string | number, actual: string | number, tolerance: number | undefined): boolean;
41
+ /** Match a resolved scalar against the authored value, under the authored tolerance. */
42
+ export declare function checkValueAssertion(reference: Reference, expected: string | number | undefined, tolerance: number | undefined, resolved: string | number): UnresolvedReference | undefined;
43
+ /** Match the resolved citation key against the authored value. The key carries its `idKind:` prefix. */
44
+ export declare function checkCitationAssertion(reference: Reference, expected: string | undefined, resolved: string): UnresolvedReference | undefined;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The assert rules of the value tier. A resolver reads a fresh value, and these functions match it
3
+ * against the authored belief.
4
+ *
5
+ * Each realization of the resolver calls these functions. Thus one semantics exists, and the fixture
6
+ * stays the executable specification of the value tier.
7
+ */
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
+ /**
20
+ * Read an authored value or a resolved cell as a finite number.
21
+ *
22
+ * A text-backed artifact such as a CSV holds every cell as a string, thus a numeric column arrives as
23
+ * `"0.01"` and not as `0.01`. A comparison that respects the JavaScript type would fail an author who
24
+ * states the number, and the arithmetic of a derivation could never run over a real CSV.
25
+ *
26
+ * The parse reads the whole string, thus `"12 genes"` stays text and never reads as 12. `Number` accepts
27
+ * the exponent form that a p-value uses, for example `"1.2e-45"`. A blank string is not a number, because
28
+ * `Number("")` gives 0 and an empty cell must never read as zero.
29
+ */
30
+ export function asFiniteNumber(value) {
31
+ if (typeof value === "number") {
32
+ return Number.isFinite(value) ? value : undefined;
33
+ }
34
+ if (value.trim() === "") {
35
+ return undefined;
36
+ }
37
+ const parsed = Number(value);
38
+ return Number.isFinite(parsed) ? parsed : undefined;
39
+ }
40
+ /**
41
+ * Match a row-filter value against a resolved cell.
42
+ *
43
+ * A text-backed artifact such as a CSV holds every cell as a string, thus a filter value of `3` must
44
+ * select the cell `"3"`. The compare reads both sides as a finite number, and it selects the row where the
45
+ * two numbers are equal. A side that is not a finite number compares by exact value, thus a text key
46
+ * selects by its exact text. The match is exact and carries no tolerance, because a filter names a
47
+ * discrete key and not a computed value with float noise.
48
+ */
49
+ export function cellMatchesFilterValue(cell, filterValue) {
50
+ if (cell === null || cell === undefined) {
51
+ return false;
52
+ }
53
+ const cellNumber = asFiniteNumber(cell);
54
+ const filterNumber = asFiniteNumber(filterValue);
55
+ if (cellNumber !== undefined && filterNumber !== undefined) {
56
+ return cellNumber === filterNumber;
57
+ }
58
+ return cell === filterValue;
59
+ }
60
+ /** Name a value with its type, so that a failed match never reads as two identical values. */
61
+ function describeValue(value) {
62
+ return typeof value === "string" ? `the string ${JSON.stringify(value)}` : `the number ${value}`;
63
+ }
64
+ /**
65
+ * Match a resolved value against an authored one.
66
+ *
67
+ * The comparison is numeric whenever both sides read as a finite number, thus the CSV cell `"0.01"`
68
+ * matches the number 0.01 that an author states. An authored tolerance is an absolute difference, and it
69
+ * is the author's own statement of how close is close enough. With no tolerance the comparison is
70
+ * relative, because an exact match would fail on the float noise of a computed value. Any other pair
71
+ * matches on exact equality.
72
+ */
73
+ export function valuesMatch(expected, actual, tolerance) {
74
+ const expectedNumber = asFiniteNumber(expected);
75
+ const actualNumber = asFiniteNumber(actual);
76
+ if (expectedNumber !== undefined && actualNumber !== undefined) {
77
+ if (tolerance !== undefined) {
78
+ return Math.abs(expectedNumber - actualNumber) <= tolerance;
79
+ }
80
+ return Math.abs(expectedNumber - actualNumber) <= RELATIVE_EPSILON * Math.max(1, Math.abs(expectedNumber), Math.abs(actualNumber));
81
+ }
82
+ return expected === actual;
83
+ }
84
+ /** Match a resolved scalar against the authored value, under the authored tolerance. */
85
+ export function checkValueAssertion(reference, expected, tolerance, resolved) {
86
+ if (expected === undefined || valuesMatch(expected, resolved, tolerance)) {
87
+ return undefined;
88
+ }
89
+ return { reference, reason: "assertion-failed", detail: `expected ${describeValue(expected)} but resolved ${describeValue(resolved)}` };
90
+ }
91
+ /** Match the resolved citation key against the authored value. The key carries its `idKind:` prefix. */
92
+ export function checkCitationAssertion(reference, expected, resolved) {
93
+ if (expected === undefined || expected === resolved) {
94
+ return undefined;
95
+ }
96
+ return { reference, reason: "assertion-failed", detail: `expected ${String(expected)} but resolved citation ${resolved}` };
97
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The one hash of a draft document.
3
+ *
4
+ * The look-before-record rule rides two durable markers, and each marker holds a hash of the draft. A
5
+ * stamp writes a hash, and a compare reads one. Both call this one function, thus two serializations of one
6
+ * document cannot split.
7
+ *
8
+ * The function serializes the draft with sorted object keys, and it hashes the text with sha256. The sort
9
+ * makes the hash independent of key order. Thus two objects with the same values and a different key order
10
+ * give one hash, and a marker survives a round trip through storage.
11
+ */
12
+ import type { DraftDocument } from "./draft.js";
13
+ /** The sha256 hash of the draft, over a JSON serialization with sorted keys. */
14
+ export declare function computeDraftHash(document: DraftDocument): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The one hash of a draft document.
3
+ *
4
+ * The look-before-record rule rides two durable markers, and each marker holds a hash of the draft. A
5
+ * stamp writes a hash, and a compare reads one. Both call this one function, thus two serializations of one
6
+ * document cannot split.
7
+ *
8
+ * The function serializes the draft with sorted object keys, and it hashes the text with sha256. The sort
9
+ * makes the hash independent of key order. Thus two objects with the same values and a different key order
10
+ * give one hash, and a marker survives a round trip through storage.
11
+ */
12
+ import { createHash } from "node:crypto";
13
+ /**
14
+ * Give back a copy of a value with each object key in sorted order, at every depth.
15
+ *
16
+ * An array keeps its order, because the order of an array is data. An object gets its keys in one canonical
17
+ * order, thus the serialization does not depend on the order that a producer wrote the keys.
18
+ */
19
+ function canonicalize(value) {
20
+ if (Array.isArray(value)) {
21
+ return value.map(canonicalize);
22
+ }
23
+ if (value !== null && typeof value === "object") {
24
+ const sorted = {};
25
+ for (const key of Object.keys(value).sort()) {
26
+ sorted[key] = canonicalize(value[key]);
27
+ }
28
+ return sorted;
29
+ }
30
+ return value;
31
+ }
32
+ /** The sha256 hash of the draft, over a JSON serialization with sorted keys. */
33
+ export function computeDraftHash(document) {
34
+ const canonical = JSON.stringify(canonicalize(document));
35
+ return createHash("sha256").update(canonical).digest("hex");
36
+ }
@@ -4,18 +4,8 @@
4
4
  * assertion rules without a host.
5
5
  */
6
6
  import { err, ok } from "neverthrow";
7
+ import { asFiniteNumber, cellMatchesFilterValue, checkCitationAssertion, checkValueAssertion } from "./assert-rules.js";
7
8
  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
9
  /** Build an `Err` that carries the unresolved reference. The `detail` key is present only when there is a detail to carry. */
20
10
  function fail(reference, reason, detail) {
21
11
  const failure = detail !== undefined ? { reference, reason, detail } : { reference, reason };
@@ -25,65 +15,6 @@ function fail(reference, reason, detail) {
25
15
  function describeReference(reference) {
26
16
  return `artifact value at ${reference.path} column ${reference.locator.column}`;
27
17
  }
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
18
  function resolveArtifactValue(reference, snapshot) {
88
19
  const artifact = snapshotEntry(snapshot, reference.path);
89
20
  if (artifact === undefined) {
@@ -111,7 +42,7 @@ function resolveArtifactValue(reference, snapshot) {
111
42
  }
112
43
  else if (locator.rowFilter !== undefined) {
113
44
  const filter = locator.rowFilter;
114
- const matches = rows.filter((row) => row[filter.column] === filter.value);
45
+ const matches = rows.filter((row) => cellMatchesFilterValue(row[filter.column], filter.value));
115
46
  if (matches.length === 0) {
116
47
  return fail(reference, "locator-out-of-range", `no row where ${filter.column} equals ${String(filter.value)}`);
117
48
  }
@@ -1,30 +1,30 @@
1
1
  /**
2
- * The mint of a report snapshot, which is the membership boundary of one report session.
2
+ * The pin of a report snapshot, which is the membership boundary of one report session.
3
3
  *
4
4
  * A report session freezes at one moment, and the analysis continues after that anchor. The snapshot
5
5
  * states which artifacts existed at the anchor, and that is a question that no other record answers.
6
6
  * Thus a session never cites an artifact that a later run produced.
7
7
  *
8
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.
9
+ * moment is not recoverable later, and the pin must run at the anchor itself.
10
10
  */
11
11
  import { type Result } from "neverthrow";
12
12
  import type { Querier } from "../state/db.js";
13
13
  import type { ReportSnapshot } from "./reference-resolver.js";
14
14
  /**
15
- * The reason that the mint gave no snapshot. The read of the ledger is the one operation that can
15
+ * The reason that the pin gave no snapshot. The read of the ledger is the one operation that can
16
16
  * fail. An analysis with no registered artifact is a normal answer, thus absence is not a member of
17
17
  * this set. `cause` carries the underlying fault for a log.
18
18
  */
19
- export type MintSnapshotError = {
19
+ export type PinSnapshotError = {
20
20
  kind: "ledger-read-failed";
21
21
  cause: unknown;
22
22
  };
23
23
  /**
24
- * Mint the snapshot of one analysis from the artifact ledger.
24
+ * Pin the snapshot of one analysis from the artifact ledger.
25
25
  *
26
- * The mint copies no cell and no byte. An entry pins identity alone, thus the snapshot grows with the
26
+ * The pin copies no cell and no byte. An entry pins identity alone, thus the snapshot grows with the
27
27
  * count of artifacts and never with the size of the data. A read of an artifact belongs to the value
28
28
  * tier, which runs one time for each report version.
29
29
  */
30
- export declare function mintReportSnapshot(pool: Querier, analysisId: string): Promise<Result<ReportSnapshot, MintSnapshotError>>;
30
+ export declare function pinReportSnapshot(pool: Querier, analysisId: string): Promise<Result<ReportSnapshot, PinSnapshotError>>;
@@ -1,23 +1,23 @@
1
1
  /**
2
- * The mint of a report snapshot, which is the membership boundary of one report session.
2
+ * The pin of a report snapshot, which is the membership boundary of one report session.
3
3
  *
4
4
  * A report session freezes at one moment, and the analysis continues after that anchor. The snapshot
5
5
  * states which artifacts existed at the anchor, and that is a question that no other record answers.
6
6
  * Thus a session never cites an artifact that a later run produced.
7
7
  *
8
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.
9
+ * moment is not recoverable later, and the pin must run at the anchor itself.
10
10
  */
11
11
  import { err, ok } from "neverthrow";
12
12
  import { queryAnalysisArtifacts } from "../state/artifacts.js";
13
13
  /**
14
- * Mint the snapshot of one analysis from the artifact ledger.
14
+ * Pin the snapshot of one analysis from the artifact ledger.
15
15
  *
16
- * The mint copies no cell and no byte. An entry pins identity alone, thus the snapshot grows with the
16
+ * The pin copies no cell and no byte. An entry pins identity alone, thus the snapshot grows with the
17
17
  * count of artifacts and never with the size of the data. A read of an artifact belongs to the value
18
18
  * tier, which runs one time for each report version.
19
19
  */
20
- export async function mintReportSnapshot(pool, analysisId) {
20
+ export async function pinReportSnapshot(pool, analysisId) {
21
21
  let ledgerRows;
22
22
  try {
23
23
  ledgerRows = await queryAnalysisArtifacts(pool, analysisId);
@@ -33,8 +33,8 @@ export async function mintReportSnapshot(pool, analysisId) {
33
33
  for (const row of ledgerRows) {
34
34
  artifacts[row.path] = { hash: row.hash, fileType: row.fileType };
35
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
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
38
  // a list would state nothing that a later read consumes.
39
39
  return ok({ artifacts });
40
40
  }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The production realization of the reference resolver. It reads the pinned artifact on disk, and it
3
+ * gives the value that the belief points at.
4
+ *
5
+ * The realization holds three layers. Layer one is identity: membership through `snapshotEntry`, and a
6
+ * streamed hash compare with `computeSha256File`. A hash mismatch fails before any parse. Layer two is
7
+ * the host fast path: an in-process parse of a CSV, a TSV, a JSON, or a parquet file at or under the cap.
8
+ * Layer three is the fall-through: an over-cap file or a host parse fault goes to the extraction arm. On any
9
+ * doubt the read falls through, thus correctness never depends on the host parser. An extension that names
10
+ * no tabular format refuses here, because the host decides the format for both arms.
11
+ *
12
+ * A read of the bytes is the expensive part, thus an optional `ArtifactReadCache` outlives one resolver. It
13
+ * holds the streamed hash and the extracted rows against a stat signature. Two tool calls of one analysis
14
+ * share one cache, and each call still binds its own extraction arm with its own auth.
15
+ *
16
+ * The realization calls the shared assert functions in `assert-rules.ts`, and it mirrors the locator walk
17
+ * and the reason choices of `fixture-resolver.ts`. Thus one semantics exists, and the fixture stays the
18
+ * executable specification of the value tier. The two realizations are substitutable behind the seam.
19
+ */
20
+ import { type ReferenceResolver } from "./reference-resolver.js";
21
+ /**
22
+ * One artifact that the host fast path could not read, addressed for the extraction arm. The `hash` pins
23
+ * the exact bytes, thus the arm reads the same artifact that the identity layer verified.
24
+ *
25
+ * `format` carries the reader that the host decided. The arm never derives a reader from the extension,
26
+ * thus the host arm and the sandbox arm can never read one file two ways.
27
+ */
28
+ export interface ExtractionRequest {
29
+ readonly path: string;
30
+ readonly hash: string;
31
+ readonly format: TabularFormat;
32
+ }
33
+ /** The rows that the extraction arm read for one artifact. Downstream the rows share the host path logic. */
34
+ export interface ExtractionArtifact {
35
+ readonly rows: Array<Record<string, string | number>>;
36
+ }
37
+ /**
38
+ * The out-of-process read arm. One `extract` call covers every fall-through artifact of one document
39
+ * pass. The result maps each requested path to its rows. A path that the arm could not read is absent
40
+ * from the map, and the reference at that path fails as `unreadable-artifact`.
41
+ *
42
+ * The arm is a seam, thus a test stubs it. While no realization is wired, a fall-through reference fails
43
+ * with `extraction-unavailable`, and the detail names the absent arm.
44
+ */
45
+ export interface ExtractionArm {
46
+ extract(requests: readonly ExtractionRequest[]): Promise<ReadonlyMap<string, ExtractionArtifact>>;
47
+ }
48
+ /**
49
+ * The disk facts of one artifact that outlive one resolver.
50
+ *
51
+ * `signature` states the bytes that the pass read: the size, the modification time, and the inode. A later
52
+ * pass compares its own stat against it before it trusts anything here, thus a file that changed on disk is
53
+ * read again. `rows` is present only for a clean read, because a read fault is transient and it must not
54
+ * stick to the analysis.
55
+ */
56
+ export interface ArtifactRead {
57
+ readonly signature: string;
58
+ readonly hash: string;
59
+ readonly rows?: Array<Record<string, string | number>>;
60
+ }
61
+ /**
62
+ * The read cache of one analysis. It holds what a read of the bytes costs: the streamed hash, and the rows
63
+ * of a file that the host could not parse in process. Two resolvers of one analysis share one cache, thus a
64
+ * preview and the record that follows it hash one unchanged file one time and start one container.
65
+ *
66
+ * The cache holds no authorization and no session value. Each resolver still binds its own extraction arm
67
+ * over the auth of its own call.
68
+ */
69
+ export interface ArtifactReadCache {
70
+ get(absolutePath: string): ArtifactRead | undefined;
71
+ set(absolutePath: string, read: ArtifactRead): void;
72
+ }
73
+ /** The read caches that the host process holds, one for each recent analysis. */
74
+ export interface ArtifactReadStore {
75
+ forAnalysis(analysisId: string): ArtifactReadCache;
76
+ }
77
+ /**
78
+ * Make the read store of the report path. One host process serves many analyses, thus the store bounds both
79
+ * dimensions: how many analyses it holds, and how many artifacts one analysis holds. A dropped entry costs
80
+ * one read again, and it is never wrong.
81
+ */
82
+ export declare function createArtifactReadStore(): ArtifactReadStore;
83
+ /**
84
+ * The construction deps of the production resolver.
85
+ *
86
+ * `workspaceRoot` and `analysisId` contain the untrusted snapshot path through `resolveWorkspacePath`,
87
+ * the same containment as the preview tool. `cap` gates the cell extraction, and it defaults to 16 MiB.
88
+ * `extractionArm` is optional, because no sandbox realization is wired yet. `readCache` is optional, thus a
89
+ * resolver with no cache reads every artifact itself and its reads die with it.
90
+ */
91
+ export interface ProductionResolverDeps {
92
+ readonly workspaceRoot: string;
93
+ readonly analysisId: string;
94
+ readonly cap?: number;
95
+ readonly extractionArm?: ExtractionArm;
96
+ readonly readCache?: ArtifactReadCache;
97
+ }
98
+ /**
99
+ * The tabular formats that the host fast path reads. The host decides which one a file is, one time, and it
100
+ * carries the decision to the extraction arm. Another extension names no format at all.
101
+ */
102
+ export type TabularFormat = "csv" | "tsv" | "json" | "parquet";
103
+ /**
104
+ * Read one value as a cell, or drop it.
105
+ *
106
+ * A parquet integer column decodes as a `bigint`, thus the coercion narrows it to a number. A `null` or an
107
+ * absent value is not a cell, thus it drops and the row omits the key. The fixture row omits an empty cell
108
+ * the same way. An object or an array is structural doubt, thus it drops too and the format falls through
109
+ * where the caller detects the loss.
110
+ *
111
+ * A non-finite number is not a cell either. A parquet float column holds a `NaN` or an `Infinity`, thus the
112
+ * coercion drops it and the row omits the key. The extraction script (`tasks/extract-values-script.ts`)
113
+ * drops the same value to `None`, thus the host arm and the sandbox arm read the same bytes the same way.
114
+ */
115
+ export declare function coerceCell(value: unknown): string | number | undefined;
116
+ /**
117
+ * Make the production resolver over the workspace root and the optional extraction arm.
118
+ *
119
+ * The resolver holds a per-pass cache. `prepare` groups the references by artifact, reads each file one
120
+ * time, and fills the cache. `resolve` then answers from the cache. A `resolve` with no prior `prepare`
121
+ * still answers, and it writes what it read into the same cache. Thus the cost is one read for each
122
+ * artifact on both paths, and never one read for each reference.
123
+ */
124
+ export declare function createProductionResolver(deps: ProductionResolverDeps): ReferenceResolver;