@secondlayer/shared 7.8.0 → 7.10.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 (52) hide show
  1. package/dist/src/archive/derived-stage-report.d.ts +99 -0
  2. package/dist/src/archive/derived-stage-report.js +64 -0
  3. package/dist/src/archive/derived-stage-report.js.map +10 -0
  4. package/dist/src/archive/disk-guard.d.ts +46 -0
  5. package/dist/src/archive/disk-guard.js +69 -0
  6. package/dist/src/archive/disk-guard.js.map +10 -0
  7. package/dist/src/archive/per-height-digest.d.ts +99 -0
  8. package/dist/src/archive/per-height-digest.js +194 -0
  9. package/dist/src/archive/per-height-digest.js.map +11 -0
  10. package/dist/src/archive/range-digest.d.ts +96 -0
  11. package/dist/src/archive/semantic-digest-builder.d.ts +1183 -0
  12. package/dist/src/archive/semantic-digest-builder.js +302 -0
  13. package/dist/src/archive/semantic-digest-builder.js.map +11 -0
  14. package/dist/src/archive/semantic-digest.d.ts +131 -0
  15. package/dist/src/archive/semantic-digest.js +143 -0
  16. package/dist/src/archive/semantic-digest.js.map +10 -0
  17. package/dist/src/coverage/index.d.ts +1670 -0
  18. package/dist/src/coverage/index.js +1483 -0
  19. package/dist/src/coverage/index.js.map +23 -0
  20. package/dist/src/db/index.d.ts +105 -1
  21. package/dist/src/db/index.js +31 -1
  22. package/dist/src/db/index.js.map +6 -5
  23. package/dist/src/db/queries/chain-reorgs.d.ts +96 -0
  24. package/dist/src/db/queries/chain-reorgs.js +30 -1
  25. package/dist/src/db/queries/chain-reorgs.js.map +6 -5
  26. package/dist/src/db/queries/contracts.d.ts +96 -0
  27. package/dist/src/db/queries/instance.d.ts +1127 -0
  28. package/dist/src/db/queries/instance.js +92 -0
  29. package/dist/src/db/queries/instance.js.map +10 -0
  30. package/dist/src/db/queries/integrity.d.ts +96 -0
  31. package/dist/src/db/queries/subgraph-gaps.d.ts +96 -0
  32. package/dist/src/db/queries/subgraph-operations.d.ts +96 -0
  33. package/dist/src/db/queries/subgraphs.d.ts +99 -1
  34. package/dist/src/db/queries/subgraphs.js +52 -23
  35. package/dist/src/db/queries/subgraphs.js.map +7 -7
  36. package/dist/src/db/queries/subscriptions.d.ts +96 -0
  37. package/dist/src/db/queries/subscriptions.js +47 -31
  38. package/dist/src/db/queries/subscriptions.js.map +5 -5
  39. package/dist/src/db/schema.d.ts +97 -1
  40. package/dist/src/index.d.ts +109 -1
  41. package/dist/src/index.js +52 -1
  42. package/dist/src/index.js.map +7 -6
  43. package/dist/src/node/client.d.ts +11 -1
  44. package/dist/src/node/client.js +42 -4
  45. package/dist/src/node/client.js.map +3 -3
  46. package/dist/src/node/local-client.d.ts +96 -0
  47. package/dist/src/node/nakamoto.d.ts +22 -7
  48. package/dist/src/node/nakamoto.js +46 -5
  49. package/dist/src/node/nakamoto.js.map +3 -3
  50. package/migrations/0115_instances.ts +33 -0
  51. package/migrations/0116_coverage_schema.ts +163 -0
  52. package/package.json +29 -1
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Derived-stage acceptance — what each service reads, writes, and needs
3
+ * beyond the canonical archive.
4
+ *
5
+ * The canonical archive rebuilds `blocks`, `transactions`, and `events`
6
+ * deterministically from R2 alone. Every stage downstream of those tables
7
+ * (decoders, subgraphs, subscription delivery) has its own inputs, outputs,
8
+ * and rebuild requirements — and an operator restoring from the archive
9
+ * needs to know exactly which of those it also needs to source, replay, or
10
+ * re-derive.
11
+ *
12
+ * This module defines the reporting shape. Each producer/service declares
13
+ * itself once (a `ServiceManifest`) and the aggregator emits one report per
14
+ * service to `reports/derived/<service>.json`. Stateful verification —
15
+ * actually re-running the service and comparing outputs — is Phase 4 work
16
+ * (see P4.10 effect manifests, P4.13 stateful deep verify); this shape is
17
+ * what Phase 4 will populate.
18
+ *
19
+ * The report answers three operator questions:
20
+ * 1. What canonical inputs does this service consume? (archive suffices)
21
+ * 2. What external inputs does it need? (RPC endpoints, envelope journal,
22
+ * contract source, ORACLE feeds — anything not in the archive)
23
+ * 3. If I restore from R2 alone, can this service resume? If not, what
24
+ * is missing?
25
+ *
26
+ * Silence is the failure mode we most want to prevent: a service that ships
27
+ * without a manifest is a service whose rebuild story is undocumented.
28
+ * `assertAllServicesDeclared()` fails a build when a known service is missing.
29
+ */
30
+ declare const DERIVED_STAGE_REPORT_SCHEMA_VERSION: 1;
31
+ type ServiceKind = "decoder" | "subgraph-runtime" | "subscription-runtime" | "protocol-producer";
32
+ type CanonicalInput = "blocks" | "transactions" | "events";
33
+ interface ExternalInput {
34
+ /** Short name for the dependency: `stacks-node-rpc`, `hiro-extended-api`,
35
+ * `observer-journal`, `contract-source-registry`, etc. */
36
+ name: string;
37
+ /** Why the archive can't provide this. One sentence. */
38
+ reason: string;
39
+ /** Where an operator can source it. `bundled-stacks`, `own-node`,
40
+ * `hiro-public-api`, `re-derive-from-events`, `manual-import`. */
41
+ source: string;
42
+ /** True if the archive supplies enough to re-derive this dependency
43
+ * without a live external source. Rare but happens (e.g. contract ABIs
44
+ * derivable from `transactions.raw_tx`). */
45
+ rebuildable_from_archive: boolean;
46
+ }
47
+ interface Output {
48
+ /** Table/topic/queue name in the operator's runtime. */
49
+ target: string;
50
+ /** How the operator verifies this output — e.g. `row-count`,
51
+ * `semantic-digest`, `effect-manifest`, `dlq-empty`. */
52
+ verification: string;
53
+ }
54
+ interface ServiceManifest {
55
+ name: string;
56
+ kind: ServiceKind;
57
+ description: string;
58
+ canonical_inputs: CanonicalInput[];
59
+ external_inputs: ExternalInput[];
60
+ outputs: Output[];
61
+ /**
62
+ * Explicit judgment on whether R2 alone can rebuild this service.
63
+ * `false` REQUIRES `external_inputs` to name what else is needed.
64
+ */
65
+ r2_alone_can_rebuild: boolean;
66
+ /** When true, this service can only replay forward from a starting point
67
+ * the operator supplies (e.g. a webhook consumer's last-delivered id).
68
+ * Publish it, don't hide it. */
69
+ requires_operator_state: boolean;
70
+ /** Repo path where the service lives, for cross-reference. */
71
+ source_path: string;
72
+ }
73
+ interface DerivedStageReport {
74
+ schema_version: typeof DERIVED_STAGE_REPORT_SCHEMA_VERSION;
75
+ generated_at: string;
76
+ services: ServiceManifest[];
77
+ }
78
+ /**
79
+ * Compose a report from a set of registered manifests. Sorts services by name
80
+ * so the JSON is stable across runs — a re-emit with unchanged manifests
81
+ * produces byte-identical output, so an operator can diff two archive
82
+ * publish cycles cleanly.
83
+ */
84
+ declare function buildDerivedStageReport(manifests: ReadonlyArray<ServiceManifest>, generatedAt: string): DerivedStageReport;
85
+ type ManifestValidationIssue = {
86
+ name: string
87
+ problem: string
88
+ };
89
+ /**
90
+ * Enforce the invariants the report shape leaves ambiguous:
91
+ * - `r2_alone_can_rebuild: false` REQUIRES at least one external input.
92
+ * Otherwise the report claims a rebuild gap it can't explain, which is
93
+ * worse than declaring nothing.
94
+ * - Names are unique — a duplicate is almost always a copy-paste that
95
+ * silently shadows the earlier declaration.
96
+ * - Every canonical_input is a real dataset name.
97
+ */
98
+ declare function validateServiceManifests(manifests: ReadonlyArray<ServiceManifest>): ManifestValidationIssue[];
99
+ export { validateServiceManifests, buildDerivedStageReport, ServiceManifest, ServiceKind, Output, ManifestValidationIssue, ExternalInput, DerivedStageReport, DERIVED_STAGE_REPORT_SCHEMA_VERSION, CanonicalInput };
@@ -0,0 +1,64 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/archive/derived-stage-report.ts
18
+ var DERIVED_STAGE_REPORT_SCHEMA_VERSION = 1;
19
+ function buildDerivedStageReport(manifests, generatedAt) {
20
+ const sorted = [...manifests].sort((a, b) => a.name.localeCompare(b.name));
21
+ return {
22
+ schema_version: DERIVED_STAGE_REPORT_SCHEMA_VERSION,
23
+ generated_at: generatedAt,
24
+ services: sorted
25
+ };
26
+ }
27
+ function validateServiceManifests(manifests) {
28
+ const issues = [];
29
+ const seen = new Set;
30
+ const canonicalDatasets = new Set([
31
+ "blocks",
32
+ "transactions",
33
+ "events"
34
+ ]);
35
+ for (const m of manifests) {
36
+ if (seen.has(m.name)) {
37
+ issues.push({ name: m.name, problem: "duplicate service name" });
38
+ }
39
+ seen.add(m.name);
40
+ if (!m.r2_alone_can_rebuild && m.external_inputs.length === 0) {
41
+ issues.push({
42
+ name: m.name,
43
+ problem: "declares r2_alone_can_rebuild=false but lists no external_inputs — the gap is undocumented"
44
+ });
45
+ }
46
+ for (const input of m.canonical_inputs) {
47
+ if (!canonicalDatasets.has(input)) {
48
+ issues.push({
49
+ name: m.name,
50
+ problem: `canonical_inputs contains unknown dataset: ${String(input)}`
51
+ });
52
+ }
53
+ }
54
+ }
55
+ return issues;
56
+ }
57
+ export {
58
+ validateServiceManifests,
59
+ buildDerivedStageReport,
60
+ DERIVED_STAGE_REPORT_SCHEMA_VERSION
61
+ };
62
+
63
+ //# debugId=C79406F0A3036B1164756E2164756E21
64
+ //# sourceMappingURL=derived-stage-report.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/archive/derived-stage-report.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Derived-stage acceptance — what each service reads, writes, and needs\n * beyond the canonical archive.\n *\n * The canonical archive rebuilds `blocks`, `transactions`, and `events`\n * deterministically from R2 alone. Every stage downstream of those tables\n * (decoders, subgraphs, subscription delivery) has its own inputs, outputs,\n * and rebuild requirements — and an operator restoring from the archive\n * needs to know exactly which of those it also needs to source, replay, or\n * re-derive.\n *\n * This module defines the reporting shape. Each producer/service declares\n * itself once (a `ServiceManifest`) and the aggregator emits one report per\n * service to `reports/derived/<service>.json`. Stateful verification —\n * actually re-running the service and comparing outputs — is Phase 4 work\n * (see P4.10 effect manifests, P4.13 stateful deep verify); this shape is\n * what Phase 4 will populate.\n *\n * The report answers three operator questions:\n * 1. What canonical inputs does this service consume? (archive suffices)\n * 2. What external inputs does it need? (RPC endpoints, envelope journal,\n * contract source, ORACLE feeds — anything not in the archive)\n * 3. If I restore from R2 alone, can this service resume? If not, what\n * is missing?\n *\n * Silence is the failure mode we most want to prevent: a service that ships\n * without a manifest is a service whose rebuild story is undocumented.\n * `assertAllServicesDeclared()` fails a build when a known service is missing.\n */\n\nexport const DERIVED_STAGE_REPORT_SCHEMA_VERSION = 1 as const;\n\nexport type ServiceKind =\n\t| \"decoder\"\n\t| \"subgraph-runtime\"\n\t| \"subscription-runtime\"\n\t| \"protocol-producer\";\n\nexport type CanonicalInput = \"blocks\" | \"transactions\" | \"events\";\n\nexport interface ExternalInput {\n\t/** Short name for the dependency: `stacks-node-rpc`, `hiro-extended-api`,\n\t * `observer-journal`, `contract-source-registry`, etc. */\n\tname: string;\n\t/** Why the archive can't provide this. One sentence. */\n\treason: string;\n\t/** Where an operator can source it. `bundled-stacks`, `own-node`,\n\t * `hiro-public-api`, `re-derive-from-events`, `manual-import`. */\n\tsource: string;\n\t/** True if the archive supplies enough to re-derive this dependency\n\t * without a live external source. Rare but happens (e.g. contract ABIs\n\t * derivable from `transactions.raw_tx`). */\n\trebuildable_from_archive: boolean;\n}\n\nexport interface Output {\n\t/** Table/topic/queue name in the operator's runtime. */\n\ttarget: string;\n\t/** How the operator verifies this output — e.g. `row-count`,\n\t * `semantic-digest`, `effect-manifest`, `dlq-empty`. */\n\tverification: string;\n}\n\nexport interface ServiceManifest {\n\tname: string;\n\tkind: ServiceKind;\n\tdescription: string;\n\tcanonical_inputs: CanonicalInput[];\n\texternal_inputs: ExternalInput[];\n\toutputs: Output[];\n\t/**\n\t * Explicit judgment on whether R2 alone can rebuild this service.\n\t * `false` REQUIRES `external_inputs` to name what else is needed.\n\t */\n\tr2_alone_can_rebuild: boolean;\n\t/** When true, this service can only replay forward from a starting point\n\t * the operator supplies (e.g. a webhook consumer's last-delivered id).\n\t * Publish it, don't hide it. */\n\trequires_operator_state: boolean;\n\t/** Repo path where the service lives, for cross-reference. */\n\tsource_path: string;\n}\n\nexport interface DerivedStageReport {\n\tschema_version: typeof DERIVED_STAGE_REPORT_SCHEMA_VERSION;\n\tgenerated_at: string;\n\tservices: ServiceManifest[];\n}\n\n/**\n * Compose a report from a set of registered manifests. Sorts services by name\n * so the JSON is stable across runs — a re-emit with unchanged manifests\n * produces byte-identical output, so an operator can diff two archive\n * publish cycles cleanly.\n */\nexport function buildDerivedStageReport(\n\tmanifests: ReadonlyArray<ServiceManifest>,\n\tgeneratedAt: string,\n): DerivedStageReport {\n\tconst sorted = [...manifests].sort((a, b) => a.name.localeCompare(b.name));\n\treturn {\n\t\tschema_version: DERIVED_STAGE_REPORT_SCHEMA_VERSION,\n\t\tgenerated_at: generatedAt,\n\t\tservices: sorted,\n\t};\n}\n\nexport type ManifestValidationIssue = {\n\tname: string;\n\tproblem: string;\n};\n\n/**\n * Enforce the invariants the report shape leaves ambiguous:\n * - `r2_alone_can_rebuild: false` REQUIRES at least one external input.\n * Otherwise the report claims a rebuild gap it can't explain, which is\n * worse than declaring nothing.\n * - Names are unique — a duplicate is almost always a copy-paste that\n * silently shadows the earlier declaration.\n * - Every canonical_input is a real dataset name.\n */\nexport function validateServiceManifests(\n\tmanifests: ReadonlyArray<ServiceManifest>,\n): ManifestValidationIssue[] {\n\tconst issues: ManifestValidationIssue[] = [];\n\tconst seen = new Set<string>();\n\tconst canonicalDatasets: ReadonlySet<CanonicalInput> = new Set([\n\t\t\"blocks\",\n\t\t\"transactions\",\n\t\t\"events\",\n\t]);\n\tfor (const m of manifests) {\n\t\tif (seen.has(m.name)) {\n\t\t\tissues.push({ name: m.name, problem: \"duplicate service name\" });\n\t\t}\n\t\tseen.add(m.name);\n\t\tif (!m.r2_alone_can_rebuild && m.external_inputs.length === 0) {\n\t\t\tissues.push({\n\t\t\t\tname: m.name,\n\t\t\t\tproblem:\n\t\t\t\t\t\"declares r2_alone_can_rebuild=false but lists no external_inputs — the gap is undocumented\",\n\t\t\t});\n\t\t}\n\t\tfor (const input of m.canonical_inputs) {\n\t\t\tif (!canonicalDatasets.has(input)) {\n\t\t\t\tissues.push({\n\t\t\t\t\tname: m.name,\n\t\t\t\t\tproblem: `canonical_inputs contains unknown dataset: ${String(input)}`,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\treturn issues;\n}\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;;AA8BO,IAAM,sCAAsC;AAiE5C,SAAS,uBAAuB,CACtC,WACA,aACqB;AAAA,EACrB,MAAM,SAAS,CAAC,GAAG,SAAS,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAAA,EACzE,OAAO;AAAA,IACN,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,UAAU;AAAA,EACX;AAAA;AAiBM,SAAS,wBAAwB,CACvC,WAC4B;AAAA,EAC5B,MAAM,SAAoC,CAAC;AAAA,EAC3C,MAAM,OAAO,IAAI;AAAA,EACjB,MAAM,oBAAiD,IAAI,IAAI;AAAA,IAC9D;AAAA,IACA;AAAA,IACA;AAAA,EACD,CAAC;AAAA,EACD,WAAW,KAAK,WAAW;AAAA,IAC1B,IAAI,KAAK,IAAI,EAAE,IAAI,GAAG;AAAA,MACrB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,yBAAyB,CAAC;AAAA,IAChE;AAAA,IACA,KAAK,IAAI,EAAE,IAAI;AAAA,IACf,IAAI,CAAC,EAAE,wBAAwB,EAAE,gBAAgB,WAAW,GAAG;AAAA,MAC9D,OAAO,KAAK;AAAA,QACX,MAAM,EAAE;AAAA,QACR,SACC;AAAA,MACF,CAAC;AAAA,IACF;AAAA,IACA,WAAW,SAAS,EAAE,kBAAkB;AAAA,MACvC,IAAI,CAAC,kBAAkB,IAAI,KAAK,GAAG;AAAA,QAClC,OAAO,KAAK;AAAA,UACX,MAAM,EAAE;AAAA,UACR,SAAS,8CAA8C,OAAO,KAAK;AAAA,QACpE,CAAC;AAAA,MACF;AAAA,IACD;AAAA,EACD;AAAA,EACA,OAAO;AAAA;",
8
+ "debugId": "C79406F0A3036B1164756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Free-space guard for bulk archive jobs.
3
+ *
4
+ * Twice on 2026-08-12 a bulk job filled the disk that production Postgres
5
+ * shares: a backfill died at 9.2M/14.4M rows on ENOSPC, and a restore died at
6
+ * 86% of events. Prod survived both, but only just — 2.9GB free with the
7
+ * indexer still ingesting.
8
+ *
9
+ * The failure mode that makes this worth guarding rather than monitoring: the
10
+ * nightly `pg_basebackup` transiently consumes ~150GB, so a job that starts
11
+ * with comfortable headroom can still hit the wall an hour later. Checking
12
+ * once at startup is not enough; the check has to be repeated as the job runs.
13
+ *
14
+ * Waiting is the right response, not failing. The spike passes, and an
15
+ * archive job that pauses for twenty minutes is strictly better than one that
16
+ * dies at 86% or, worse, takes the database down with it.
17
+ */
18
+ type DiskSpace = {
19
+ freeBytes: number
20
+ totalBytes: number
21
+ };
22
+ declare function readDiskSpace(path: string): Promise<DiskSpace>;
23
+ type DiskGuardOptions = {
24
+ /** Any path on the filesystem to watch. */
25
+ path: string
26
+ /** Pause below this many bytes free. */
27
+ minFreeBytes?: number
28
+ /** How long to wait between re-checks while paused. */
29
+ pollMs?: number
30
+ /** Give up after this long rather than pausing forever. */
31
+ maxWaitMs?: number
32
+ onPause?: (free: DiskSpace, waitedMs: number) => void
33
+ sleep?: (ms: number) => Promise<void>
34
+ readSpace?: (path: string) => Promise<DiskSpace>
35
+ };
36
+ declare class DiskSpaceExhausted extends Error {
37
+ constructor(free: number, required: number, waitedMs: number);
38
+ }
39
+ /**
40
+ * Block until there is enough headroom, or throw after `maxWaitMs`.
41
+ *
42
+ * Call this between units of work — before each partition, not once at the
43
+ * start — because the pressure that matters arrives mid-job.
44
+ */
45
+ declare function waitForDiskSpace(options: DiskGuardOptions): Promise<void>;
46
+ export { waitForDiskSpace, readDiskSpace, DiskSpaceExhausted, DiskSpace, DiskGuardOptions };
@@ -0,0 +1,69 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/archive/disk-guard.ts
18
+ import { statfs } from "node:fs/promises";
19
+ async function readDiskSpace(path) {
20
+ const stats = await statfs(path);
21
+ return {
22
+ freeBytes: stats.bavail * stats.bsize,
23
+ totalBytes: stats.blocks * stats.bsize
24
+ };
25
+ }
26
+ var DEFAULT_MIN_FREE_BYTES = 100 * 1024 ** 3;
27
+ var DEFAULT_POLL_MS = 60000;
28
+ var DEFAULT_MAX_WAIT_MS = 2 * 3600000;
29
+ var MAX_THRESHOLD_FRACTION_OF_DISK = 0.25;
30
+ function effectiveThreshold(configured, totalBytes) {
31
+ if (!Number.isFinite(totalBytes) || totalBytes <= 0)
32
+ return configured;
33
+ return Math.min(configured, totalBytes * MAX_THRESHOLD_FRACTION_OF_DISK);
34
+ }
35
+
36
+ class DiskSpaceExhausted extends Error {
37
+ constructor(free, required, waitedMs) {
38
+ super(`only ${(free / 1024 ** 3).toFixed(1)}GB free (need ${(required / 1024 ** 3).toFixed(1)}GB) after waiting ${Math.round(waitedMs / 60000)}m`);
39
+ this.name = "DiskSpaceExhausted";
40
+ }
41
+ }
42
+ async function waitForDiskSpace(options) {
43
+ const minFree = options.minFreeBytes ?? DEFAULT_MIN_FREE_BYTES;
44
+ const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
45
+ const maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
46
+ const readSpace = options.readSpace ?? readDiskSpace;
47
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
48
+ let waited = 0;
49
+ while (true) {
50
+ const space = await readSpace(options.path);
51
+ const threshold = effectiveThreshold(minFree, space.totalBytes);
52
+ if (space.freeBytes >= threshold)
53
+ return;
54
+ if (waited >= maxWaitMs) {
55
+ throw new DiskSpaceExhausted(space.freeBytes, threshold, waited);
56
+ }
57
+ options.onPause?.(space, waited);
58
+ await sleep(pollMs);
59
+ waited += pollMs;
60
+ }
61
+ }
62
+ export {
63
+ waitForDiskSpace,
64
+ readDiskSpace,
65
+ DiskSpaceExhausted
66
+ };
67
+
68
+ //# debugId=F49523DF35B78F5364756E2164756E21
69
+ //# sourceMappingURL=disk-guard.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/archive/disk-guard.ts"],
4
+ "sourcesContent": [
5
+ "import { statfs } from \"node:fs/promises\";\n\n/**\n * Free-space guard for bulk archive jobs.\n *\n * Twice on 2026-08-12 a bulk job filled the disk that production Postgres\n * shares: a backfill died at 9.2M/14.4M rows on ENOSPC, and a restore died at\n * 86% of events. Prod survived both, but only just — 2.9GB free with the\n * indexer still ingesting.\n *\n * The failure mode that makes this worth guarding rather than monitoring: the\n * nightly `pg_basebackup` transiently consumes ~150GB, so a job that starts\n * with comfortable headroom can still hit the wall an hour later. Checking\n * once at startup is not enough; the check has to be repeated as the job runs.\n *\n * Waiting is the right response, not failing. The spike passes, and an\n * archive job that pauses for twenty minutes is strictly better than one that\n * dies at 86% or, worse, takes the database down with it.\n */\n\nexport type DiskSpace = { freeBytes: number; totalBytes: number };\n\nexport async function readDiskSpace(path: string): Promise<DiskSpace> {\n\tconst stats = await statfs(path);\n\treturn {\n\t\tfreeBytes: stats.bavail * stats.bsize,\n\t\ttotalBytes: stats.blocks * stats.bsize,\n\t};\n}\n\nexport type DiskGuardOptions = {\n\t/** Any path on the filesystem to watch. */\n\tpath: string;\n\t/** Pause below this many bytes free. */\n\tminFreeBytes?: number;\n\t/** How long to wait between re-checks while paused. */\n\tpollMs?: number;\n\t/** Give up after this long rather than pausing forever. */\n\tmaxWaitMs?: number;\n\tonPause?: (free: DiskSpace, waitedMs: number) => void;\n\tsleep?: (ms: number) => Promise<void>;\n\treadSpace?: (path: string) => Promise<DiskSpace>;\n};\n\n/** 100GB: comfortably above the ~39GB an export needs plus the backup spike. */\nconst DEFAULT_MIN_FREE_BYTES = 100 * 1024 ** 3;\nconst DEFAULT_POLL_MS = 60_000;\nconst DEFAULT_MAX_WAIT_MS = 2 * 3_600_000;\n/**\n * A threshold larger than a share of the whole disk is unsatisfiable, not\n * protective: on a 14GB CI runner or a modest VPS, demanding 100GB free would\n * block forever waiting for space that machine can never have. The absolute\n * default is tuned for the production host; this keeps it from becoming a wall\n * everywhere else.\n */\nconst MAX_THRESHOLD_FRACTION_OF_DISK = 0.25;\n\nfunction effectiveThreshold(configured: number, totalBytes: number): number {\n\tif (!Number.isFinite(totalBytes) || totalBytes <= 0) return configured;\n\treturn Math.min(configured, totalBytes * MAX_THRESHOLD_FRACTION_OF_DISK);\n}\n\nexport class DiskSpaceExhausted extends Error {\n\tconstructor(free: number, required: number, waitedMs: number) {\n\t\tsuper(\n\t\t\t`only ${(free / 1024 ** 3).toFixed(1)}GB free (need ${(required / 1024 ** 3).toFixed(1)}GB) after waiting ${Math.round(waitedMs / 60_000)}m`,\n\t\t);\n\t\tthis.name = \"DiskSpaceExhausted\";\n\t}\n}\n\n/**\n * Block until there is enough headroom, or throw after `maxWaitMs`.\n *\n * Call this between units of work — before each partition, not once at the\n * start — because the pressure that matters arrives mid-job.\n */\nexport async function waitForDiskSpace(\n\toptions: DiskGuardOptions,\n): Promise<void> {\n\tconst minFree = options.minFreeBytes ?? DEFAULT_MIN_FREE_BYTES;\n\tconst pollMs = options.pollMs ?? DEFAULT_POLL_MS;\n\tconst maxWaitMs = options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;\n\tconst readSpace = options.readSpace ?? readDiskSpace;\n\tconst sleep =\n\t\toptions.sleep ??\n\t\t((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));\n\n\tlet waited = 0;\n\twhile (true) {\n\t\tconst space = await readSpace(options.path);\n\t\tconst threshold = effectiveThreshold(minFree, space.totalBytes);\n\t\tif (space.freeBytes >= threshold) return;\n\t\tif (waited >= maxWaitMs) {\n\t\t\tthrow new DiskSpaceExhausted(space.freeBytes, threshold, waited);\n\t\t}\n\t\toptions.onPause?.(space, waited);\n\t\tawait sleep(pollMs);\n\t\twaited += pollMs;\n\t}\n}\n"
6
+ ],
7
+ "mappings": ";;;;;;;;;;;;;;;;;AAAA;AAsBA,eAAsB,aAAa,CAAC,MAAkC;AAAA,EACrE,MAAM,QAAQ,MAAM,OAAO,IAAI;AAAA,EAC/B,OAAO;AAAA,IACN,WAAW,MAAM,SAAS,MAAM;AAAA,IAChC,YAAY,MAAM,SAAS,MAAM;AAAA,EAClC;AAAA;AAkBD,IAAM,yBAAyB,MAAM,QAAQ;AAC7C,IAAM,kBAAkB;AACxB,IAAM,sBAAsB,IAAI;AAQhC,IAAM,iCAAiC;AAEvC,SAAS,kBAAkB,CAAC,YAAoB,YAA4B;AAAA,EAC3E,IAAI,CAAC,OAAO,SAAS,UAAU,KAAK,cAAc;AAAA,IAAG,OAAO;AAAA,EAC5D,OAAO,KAAK,IAAI,YAAY,aAAa,8BAA8B;AAAA;AAAA;AAGjE,MAAM,2BAA2B,MAAM;AAAA,EAC7C,WAAW,CAAC,MAAc,UAAkB,UAAkB;AAAA,IAC7D,MACC,SAAS,OAAO,QAAQ,GAAG,QAAQ,CAAC,mBAAmB,WAAW,QAAQ,GAAG,QAAQ,CAAC,sBAAsB,KAAK,MAAM,WAAW,KAAM,IACzI;AAAA,IACA,KAAK,OAAO;AAAA;AAEd;AAQA,eAAsB,gBAAgB,CACrC,SACgB;AAAA,EAChB,MAAM,UAAU,QAAQ,gBAAgB;AAAA,EACxC,MAAM,SAAS,QAAQ,UAAU;AAAA,EACjC,MAAM,YAAY,QAAQ,aAAa;AAAA,EACvC,MAAM,YAAY,QAAQ,aAAa;AAAA,EACvC,MAAM,QACL,QAAQ,UACP,CAAC,OAAe,IAAI,QAAc,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,EAExE,IAAI,SAAS;AAAA,EACb,OAAO,MAAM;AAAA,IACZ,MAAM,QAAQ,MAAM,UAAU,QAAQ,IAAI;AAAA,IAC1C,MAAM,YAAY,mBAAmB,SAAS,MAAM,UAAU;AAAA,IAC9D,IAAI,MAAM,aAAa;AAAA,MAAW;AAAA,IAClC,IAAI,UAAU,WAAW;AAAA,MACxB,MAAM,IAAI,mBAAmB,MAAM,WAAW,WAAW,MAAM;AAAA,IAChE;AAAA,IACA,QAAQ,UAAU,OAAO,MAAM;AAAA,IAC/B,MAAM,MAAM,MAAM;AAAA,IAClB,UAAU;AAAA,EACX;AAAA;",
8
+ "debugId": "F49523DF35B78F5364756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * v1 canonical byte encoding for chain identity — the shape that lets
3
+ * independently-run runtimes agree on the same block.
4
+ *
5
+ * The exact-body digest (`sha256:parquet-object`) proves capture fidelity to one
6
+ * source: replay the same bytes, get the same rows. It cannot prove two nodes
7
+ * observed the same chain, because their Parquet encoders legally differ in
8
+ * column padding, compression, JSON key order, and null placement — two
9
+ * operators processing the same block can ship byte-different Parquets that
10
+ * carry identical content.
11
+ *
12
+ * The semantic digest is the shape that lets independent runtimes agree. Every
13
+ * value on a block/transaction/event is normalized into a canonical byte string
14
+ * by rules that MUST NOT change without a version bump. Two v1 computations
15
+ * over the same range agree, or one is wrong about the chain — never about
16
+ * formatting.
17
+ *
18
+ * Encoding rules (v1):
19
+ * - Fields join with `\x1f` (ASCII US, unit separator — cannot legally
20
+ * appear in any chain value; a printable delimiter risks collision with
21
+ * contract-emitted strings).
22
+ * - Null encodes as `\x00` (single NUL). Distinct from empty string `""`.
23
+ * - Integers encode as their base-10 string. bigint → decimal string.
24
+ * - JSON payload columns (function_args, event data) encode as canonical
25
+ * JSON: object keys sorted lexicographically, no whitespace, bigint →
26
+ * quoted decimal string, non-finite numbers rejected.
27
+ * - Field order per record type is fixed and never reordered.
28
+ * - Digest = sha256(bytes), lowercase hex.
29
+ *
30
+ * Rollup rules (v1) — order MUST match the archive's stream order or an
31
+ * honest re-will disagree with the manifest:
32
+ * - blocks: (height asc)
33
+ * - transactions: (block_height asc, tx_index asc, tx_id asc)
34
+ * - events: (block_height asc, event_index asc, tx_id asc)
35
+ * Per-partition rollup is `sha256(concat(row_digest_bytes))` in stream order.
36
+ *
37
+ * Test vectors under `packages/shared/src/archive/__fixtures__/semantic-digest/`
38
+ * pin the encoding. Any change to bytes must bump the spec version.
39
+ */
40
+ declare const SEMANTIC_DIGEST_SPEC_V1: "sha256:semantic-v1";
41
+ /**
42
+ * Per-height semantic digest sidecars — enable "verify one block" without
43
+ * downloading a whole 50k-block partition.
44
+ *
45
+ * The partition-level semantic digest is the shape a byte-level comparator
46
+ * wants: one string per partition, cheap to compare in bulk. But a caller who
47
+ * only cares about block 8_500_017 has to pull an entire Parquet file to check
48
+ * one row. The per-height sidecar closes that gap: one 32-byte block digest and
49
+ * two roll-up digests per height (transactions and events), addressable by
50
+ * height without touching the raw datasets.
51
+ *
52
+ * Rollup rules (v1) — MUST match the exporter's stream order or an honest
53
+ * re-derivation will disagree:
54
+ * - transactions_rollup: sha256(concat(tx_digest_bytes, tx_index asc, tx_id asc))
55
+ * `null` when the height has no transactions.
56
+ * - events_rollup: sha256(concat(event_digest_bytes, event_index asc, tx_id asc))
57
+ * `null` when the height has no events.
58
+ * - block_digest: the semantic-v1 block digest itself (one row per
59
+ * height, so no rollup needed).
60
+ *
61
+ * `null` is deliberate: an empty rollup is not the sha256 of nothing (which
62
+ * would collide across two empty datasets); an absent-here-but-present-elsewhere
63
+ * dataset must never compare equal to a truly-empty one.
64
+ */
65
+ interface PerHeightDigestRow {
66
+ height: number;
67
+ block_digest: string;
68
+ transactions_rollup: string | null;
69
+ events_rollup: string | null;
70
+ }
71
+ declare class PerHeightDigestAccumulator {
72
+ private readonly rows;
73
+ setBlockDigest(height: number, blockDigestHex: string): void;
74
+ appendTransactionDigest(height: number, digestHex: string): void;
75
+ appendEventDigest(height: number, digestHex: string): void;
76
+ /**
77
+ * Drain the accumulator into height-ascending rows. Heights with no block
78
+ * digest are dropped with a thrown error — a canonical partition MUST have
79
+ * a block at every height it covers, and silently emitting a row without
80
+ * one would hide the missing block from downstream verifiers.
81
+ */
82
+ drain(fromBlock: number, toBlock: number): PerHeightDigestRow[];
83
+ spec(): typeof SEMANTIC_DIGEST_SPEC_V1;
84
+ private ensure;
85
+ }
86
+ /**
87
+ * Registered in the snapshot manifest under `digest_index[]`. One entry per
88
+ * partition, addressable at height granularity without downloading raw data.
89
+ */
90
+ interface DigestIndexPartition {
91
+ from_block: number;
92
+ to_block: number;
93
+ path: string;
94
+ row_count: number;
95
+ byte_size: number;
96
+ sha256: string;
97
+ digest_spec: typeof SEMANTIC_DIGEST_SPEC_V1;
98
+ }
99
+ export { PerHeightDigestRow, PerHeightDigestAccumulator, DigestIndexPartition };
@@ -0,0 +1,194 @@
1
+ import { createRequire } from "node:module";
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+
17
+ // src/archive/semantic-digest.ts
18
+ import { createHash } from "node:crypto";
19
+ var SEMANTIC_DIGEST_SPEC_V1 = "sha256:semantic-v1";
20
+ var FIELD_SEP = "\x1F";
21
+ var NULL_MARKER = "\x00";
22
+ var textEncoder = new TextEncoder;
23
+ function canonicalJson(value) {
24
+ if (value === null)
25
+ return "null";
26
+ if (typeof value === "string" || typeof value === "boolean") {
27
+ return JSON.stringify(value);
28
+ }
29
+ if (typeof value === "number") {
30
+ if (!Number.isFinite(value)) {
31
+ throw new Error("canonicalJson: non-finite number");
32
+ }
33
+ return JSON.stringify(value);
34
+ }
35
+ if (typeof value === "bigint")
36
+ return JSON.stringify(value.toString());
37
+ if (Array.isArray(value)) {
38
+ return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
39
+ }
40
+ if (typeof value === "object") {
41
+ const object = value;
42
+ return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
43
+ }
44
+ return "null";
45
+ }
46
+ function scalar(value) {
47
+ if (value === null || value === undefined)
48
+ return NULL_MARKER;
49
+ if (typeof value === "number") {
50
+ if (!Number.isFinite(value)) {
51
+ throw new Error("semanticDigest: non-finite number");
52
+ }
53
+ if (!Number.isInteger(value)) {
54
+ throw new Error(`semanticDigest: non-integer scalar ${value}`);
55
+ }
56
+ return String(value);
57
+ }
58
+ if (typeof value === "bigint")
59
+ return value.toString();
60
+ return value;
61
+ }
62
+ function jsonScalar(value) {
63
+ return value === null || value === undefined ? NULL_MARKER : canonicalJson(value);
64
+ }
65
+ function sha256Hex(input) {
66
+ return createHash("sha256").update(input).digest("hex");
67
+ }
68
+ function encodeBlock(row) {
69
+ return [
70
+ scalar(row.height),
71
+ scalar(row.hash),
72
+ scalar(row.parent_hash),
73
+ scalar(row.burn_block_height),
74
+ scalar(row.burn_block_hash),
75
+ scalar(row.index_block_hash),
76
+ scalar(row.timestamp)
77
+ ].join(FIELD_SEP);
78
+ }
79
+ function encodeTransaction(row) {
80
+ return [
81
+ scalar(row.tx_id),
82
+ scalar(row.block_height),
83
+ scalar(row.tx_index),
84
+ scalar(row.type),
85
+ scalar(row.sender),
86
+ scalar(row.status),
87
+ scalar(row.contract_id),
88
+ scalar(row.function_name),
89
+ jsonScalar(row.function_args),
90
+ scalar(row.raw_result),
91
+ scalar(row.raw_tx)
92
+ ].join(FIELD_SEP);
93
+ }
94
+ function encodeEvent(row) {
95
+ return [
96
+ scalar(row.tx_id),
97
+ scalar(row.block_height),
98
+ scalar(row.event_index),
99
+ scalar(row.type),
100
+ jsonScalar(row.data)
101
+ ].join(FIELD_SEP);
102
+ }
103
+ var semanticDigest = {
104
+ v1: {
105
+ spec: SEMANTIC_DIGEST_SPEC_V1,
106
+ encodeBlock,
107
+ encodeTransaction,
108
+ encodeEvent,
109
+ block: (row) => sha256Hex(textEncoder.encode(encodeBlock(row))),
110
+ transaction: (row) => sha256Hex(textEncoder.encode(encodeTransaction(row))),
111
+ event: (row) => sha256Hex(textEncoder.encode(encodeEvent(row)))
112
+ }
113
+ };
114
+
115
+ class SemanticDigestRollup {
116
+ hasher = createHash("sha256");
117
+ count = 0;
118
+ static forDataset(_dataset) {
119
+ return new SemanticDigestRollup;
120
+ }
121
+ appendRowDigest(rowDigestHex) {
122
+ this.hasher.update(Buffer.from(rowDigestHex, "hex"));
123
+ this.count += 1;
124
+ }
125
+ rowCount() {
126
+ return this.count;
127
+ }
128
+ digest() {
129
+ return this.count === 0 ? null : this.hasher.copy().digest("hex");
130
+ }
131
+ spec() {
132
+ return SEMANTIC_DIGEST_SPEC_V1;
133
+ }
134
+ }
135
+
136
+ // src/archive/per-height-digest.ts
137
+ import { createHash as createHash2 } from "node:crypto";
138
+ class PerHeightDigestAccumulator {
139
+ rows = new Map;
140
+ setBlockDigest(height, blockDigestHex) {
141
+ this.ensure(height).block_digest = blockDigestHex;
142
+ }
143
+ appendTransactionDigest(height, digestHex) {
144
+ const a = this.ensure(height);
145
+ a.tx.update(Buffer.from(digestHex, "hex"));
146
+ a.tx_count += 1;
147
+ }
148
+ appendEventDigest(height, digestHex) {
149
+ const a = this.ensure(height);
150
+ a.event.update(Buffer.from(digestHex, "hex"));
151
+ a.event_count += 1;
152
+ }
153
+ drain(fromBlock, toBlock) {
154
+ const rows = [];
155
+ for (let h = fromBlock;h <= toBlock; h++) {
156
+ const a = this.rows.get(h);
157
+ if (!a)
158
+ continue;
159
+ if (!a.block_digest) {
160
+ throw new Error(`per-height digest at ${h} has tx/event rollups but no block digest`);
161
+ }
162
+ rows.push({
163
+ height: h,
164
+ block_digest: a.block_digest,
165
+ transactions_rollup: a.tx_count === 0 ? null : a.tx.copy().digest("hex"),
166
+ events_rollup: a.event_count === 0 ? null : a.event.copy().digest("hex")
167
+ });
168
+ }
169
+ return rows;
170
+ }
171
+ spec() {
172
+ return SEMANTIC_DIGEST_SPEC_V1;
173
+ }
174
+ ensure(height) {
175
+ let a = this.rows.get(height);
176
+ if (!a) {
177
+ a = {
178
+ block_digest: null,
179
+ tx: createHash2("sha256"),
180
+ tx_count: 0,
181
+ event: createHash2("sha256"),
182
+ event_count: 0
183
+ };
184
+ this.rows.set(height, a);
185
+ }
186
+ return a;
187
+ }
188
+ }
189
+ export {
190
+ PerHeightDigestAccumulator
191
+ };
192
+
193
+ //# debugId=2ABEAEFAD6B6D8C464756E2164756E21
194
+ //# sourceMappingURL=per-height-digest.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/archive/semantic-digest.ts", "../src/archive/per-height-digest.ts"],
4
+ "sourcesContent": [
5
+ "import { createHash } from \"node:crypto\";\n\n/**\n * v1 canonical byte encoding for chain identity — the shape that lets\n * independently-run runtimes agree on the same block.\n *\n * The exact-body digest (`sha256:parquet-object`) proves capture fidelity to one\n * source: replay the same bytes, get the same rows. It cannot prove two nodes\n * observed the same chain, because their Parquet encoders legally differ in\n * column padding, compression, JSON key order, and null placement — two\n * operators processing the same block can ship byte-different Parquets that\n * carry identical content.\n *\n * The semantic digest is the shape that lets independent runtimes agree. Every\n * value on a block/transaction/event is normalized into a canonical byte string\n * by rules that MUST NOT change without a version bump. Two v1 computations\n * over the same range agree, or one is wrong about the chain — never about\n * formatting.\n *\n * Encoding rules (v1):\n * - Fields join with `\\x1f` (ASCII US, unit separator — cannot legally\n * appear in any chain value; a printable delimiter risks collision with\n * contract-emitted strings).\n * - Null encodes as `\\x00` (single NUL). Distinct from empty string `\"\"`.\n * - Integers encode as their base-10 string. bigint → decimal string.\n * - JSON payload columns (function_args, event data) encode as canonical\n * JSON: object keys sorted lexicographically, no whitespace, bigint →\n * quoted decimal string, non-finite numbers rejected.\n * - Field order per record type is fixed and never reordered.\n * - Digest = sha256(bytes), lowercase hex.\n *\n * Rollup rules (v1) — order MUST match the archive's stream order or an\n * honest re-export will disagree with the manifest:\n * - blocks: (height asc)\n * - transactions: (block_height asc, tx_index asc, tx_id asc)\n * - events: (block_height asc, event_index asc, tx_id asc)\n * Per-partition rollup is `sha256(concat(row_digest_bytes))` in stream order.\n *\n * Test vectors under `packages/shared/src/archive/__fixtures__/semantic-digest/`\n * pin the encoding. Any change to bytes must bump the spec version.\n */\n\nexport const SEMANTIC_DIGEST_SPEC_V1 = \"sha256:semantic-v1\" as const;\nexport type SemanticDigestSpec = typeof SEMANTIC_DIGEST_SPEC_V1;\n\nconst FIELD_SEP = \"\\x1f\";\nconst NULL_MARKER = \"\\x00\";\n\nconst textEncoder = new TextEncoder();\n\n/**\n * Stable JSON — sorted keys, no whitespace, bigint → decimal string. Distinct\n * from `JSON.stringify(x)` which respects insertion order and rejects bigints.\n * Also rejects non-finite numbers, since NaN/Infinity have no on-chain meaning\n * and would silently hash as `null` under `JSON.stringify`.\n */\nexport function canonicalJson(value: unknown): string {\n\tif (value === null) return \"null\";\n\tif (typeof value === \"string\" || typeof value === \"boolean\") {\n\t\treturn JSON.stringify(value);\n\t}\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) {\n\t\t\tthrow new Error(\"canonicalJson: non-finite number\");\n\t\t}\n\t\treturn JSON.stringify(value);\n\t}\n\tif (typeof value === \"bigint\") return JSON.stringify(value.toString());\n\tif (Array.isArray(value)) {\n\t\treturn `[${value.map((item) => canonicalJson(item)).join(\",\")}]`;\n\t}\n\tif (typeof value === \"object\") {\n\t\tconst object = value as Record<string, unknown>;\n\t\treturn `{${Object.keys(object)\n\t\t\t.sort()\n\t\t\t.map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`)\n\t\t\t.join(\",\")}}`;\n\t}\n\t// undefined / function / symbol — never present in decoded chain rows.\n\t// Fall through as null so tests catch unexpected shapes.\n\treturn \"null\";\n}\n\nfunction scalar(value: string | number | bigint | null | undefined): string {\n\tif (value === null || value === undefined) return NULL_MARKER;\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) {\n\t\t\tthrow new Error(\"semanticDigest: non-finite number\");\n\t\t}\n\t\tif (!Number.isInteger(value)) {\n\t\t\t// Chain heights, indexes, timestamps are all integer; a fractional\n\t\t\t// value here is a schema bug we want to surface loudly, not silently\n\t\t\t// serialize with float precision that varies by runtime.\n\t\t\tthrow new Error(`semanticDigest: non-integer scalar ${value}`);\n\t\t}\n\t\treturn String(value);\n\t}\n\tif (typeof value === \"bigint\") return value.toString();\n\treturn value;\n}\n\nfunction jsonScalar(value: unknown): string {\n\treturn value === null || value === undefined\n\t\t? NULL_MARKER\n\t\t: canonicalJson(value);\n}\n\nfunction sha256Hex(input: Uint8Array | string): string {\n\treturn createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\nexport interface BlockDigestInput {\n\theight: number | bigint;\n\thash: string;\n\tparent_hash: string;\n\tburn_block_height: number | bigint;\n\tburn_block_hash: string | null;\n\tindex_block_hash: string | null;\n\ttimestamp: number | bigint;\n}\n\nexport interface TransactionDigestInput {\n\ttx_id: string;\n\tblock_height: number | bigint;\n\ttx_index: number;\n\ttype: string;\n\tsender: string;\n\tstatus: string;\n\tcontract_id: string | null;\n\tfunction_name: string | null;\n\tfunction_args: unknown;\n\traw_result: string | null;\n\traw_tx: string;\n}\n\nexport interface EventDigestInput {\n\ttx_id: string;\n\tblock_height: number | bigint;\n\tevent_index: number;\n\ttype: string;\n\tdata: unknown;\n}\n\nfunction encodeBlock(row: BlockDigestInput): string {\n\treturn [\n\t\tscalar(row.height),\n\t\tscalar(row.hash),\n\t\tscalar(row.parent_hash),\n\t\tscalar(row.burn_block_height),\n\t\tscalar(row.burn_block_hash),\n\t\tscalar(row.index_block_hash),\n\t\tscalar(row.timestamp),\n\t].join(FIELD_SEP);\n}\n\nfunction encodeTransaction(row: TransactionDigestInput): string {\n\treturn [\n\t\tscalar(row.tx_id),\n\t\tscalar(row.block_height),\n\t\tscalar(row.tx_index),\n\t\tscalar(row.type),\n\t\tscalar(row.sender),\n\t\tscalar(row.status),\n\t\tscalar(row.contract_id),\n\t\tscalar(row.function_name),\n\t\tjsonScalar(row.function_args),\n\t\tscalar(row.raw_result),\n\t\tscalar(row.raw_tx),\n\t].join(FIELD_SEP);\n}\n\nfunction encodeEvent(row: EventDigestInput): string {\n\treturn [\n\t\tscalar(row.tx_id),\n\t\tscalar(row.block_height),\n\t\tscalar(row.event_index),\n\t\tscalar(row.type),\n\t\tjsonScalar(row.data),\n\t].join(FIELD_SEP);\n}\n\n/**\n * v1 per-row digests. Each is `sha256(encoded_row_bytes)` in lowercase hex.\n * The encoded byte form is stable across runtimes; the sha256 is stable\n * across CPU architectures. Never mutate an encoder without a v-bump.\n */\nexport interface SemanticDigestV1 {\n\treadonly spec: SemanticDigestSpec;\n\treadonly encodeBlock: (row: BlockDigestInput) => string;\n\treadonly encodeTransaction: (row: TransactionDigestInput) => string;\n\treadonly encodeEvent: (row: EventDigestInput) => string;\n\treadonly block: (row: BlockDigestInput) => string;\n\treadonly transaction: (row: TransactionDigestInput) => string;\n\treadonly event: (row: EventDigestInput) => string;\n}\n\nexport interface SemanticDigestApi {\n\treadonly v1: SemanticDigestV1;\n}\n\nexport const semanticDigest: SemanticDigestApi = {\n\tv1: {\n\t\tspec: SEMANTIC_DIGEST_SPEC_V1,\n\t\tencodeBlock,\n\t\tencodeTransaction,\n\t\tencodeEvent,\n\t\tblock: (row: BlockDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeBlock(row))),\n\t\ttransaction: (row: TransactionDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeTransaction(row))),\n\t\tevent: (row: EventDigestInput): string =>\n\t\t\tsha256Hex(textEncoder.encode(encodeEvent(row))),\n\t},\n};\n\n/**\n * Streaming rollup — feed rows one at a time in their declared order, read the\n * rolled digest at the end. Two callers passing the same rows in the same order\n * produce the same rollup, regardless of batching.\n *\n * The rollup composes: partition_digest = sha256(concat(row_digest_bytes)),\n * where each row_digest_bytes = sha256(encode(row)). This is why the streaming\n * implementation can be constant-memory — the outer sha256 accepts row digests\n * incrementally.\n */\nexport class SemanticDigestRollup {\n\tprivate readonly hasher = createHash(\"sha256\");\n\tprivate count = 0;\n\n\tstatic forDataset(\n\t\t_dataset: \"blocks\" | \"transactions\" | \"events\",\n\t): SemanticDigestRollup {\n\t\treturn new SemanticDigestRollup();\n\t}\n\n\tappendRowDigest(rowDigestHex: string): void {\n\t\tthis.hasher.update(Buffer.from(rowDigestHex, \"hex\"));\n\t\tthis.count += 1;\n\t}\n\n\trowCount(): number {\n\t\treturn this.count;\n\t}\n\n\t/** Empty rollups return `null`, not the sha256 of nothing, so an empty\n\t * range never compares equal to another empty range on a different\n\t * dataset. Callers assign `null` semantics deliberately. */\n\tdigest(): string | null {\n\t\treturn this.count === 0 ? null : this.hasher.copy().digest(\"hex\");\n\t}\n\n\tspec(): SemanticDigestSpec {\n\t\treturn SEMANTIC_DIGEST_SPEC_V1;\n\t}\n}\n\n/**\n * Per-partition digest record — the shape that lands in the snapshot manifest.\n * Complements the existing `sha256:parquet-object` per-partition digest by\n * proving semantic identity independent of file encoding.\n */\nexport interface PartitionSemanticDigest {\n\tdataset: \"blocks\" | \"transactions\" | \"events\";\n\tfrom_block: number;\n\tto_block: number;\n\trow_count: number;\n\tdigest: string | null;\n\tdigest_spec: SemanticDigestSpec;\n}\n",
6
+ "import { createHash } from \"node:crypto\";\nimport { SEMANTIC_DIGEST_SPEC_V1 } from \"./semantic-digest.ts\";\n\n/**\n * Per-height semantic digest sidecars — enable \"verify one block\" without\n * downloading a whole 50k-block partition.\n *\n * The partition-level semantic digest is the shape a byte-level comparator\n * wants: one string per partition, cheap to compare in bulk. But a caller who\n * only cares about block 8_500_017 has to pull an entire Parquet file to check\n * one row. The per-height sidecar closes that gap: one 32-byte block digest and\n * two roll-up digests per height (transactions and events), addressable by\n * height without touching the raw datasets.\n *\n * Rollup rules (v1) — MUST match the exporter's stream order or an honest\n * re-derivation will disagree:\n * - transactions_rollup: sha256(concat(tx_digest_bytes, tx_index asc, tx_id asc))\n * `null` when the height has no transactions.\n * - events_rollup: sha256(concat(event_digest_bytes, event_index asc, tx_id asc))\n * `null` when the height has no events.\n * - block_digest: the semantic-v1 block digest itself (one row per\n * height, so no rollup needed).\n *\n * `null` is deliberate: an empty rollup is not the sha256 of nothing (which\n * would collide across two empty datasets); an absent-here-but-present-elsewhere\n * dataset must never compare equal to a truly-empty one.\n */\n\nexport interface PerHeightDigestRow {\n\theight: number;\n\tblock_digest: string;\n\ttransactions_rollup: string | null;\n\tevents_rollup: string | null;\n}\n\ninterface Accumulator {\n\tblock_digest: string | null;\n\ttx: ReturnType<typeof createHash>;\n\ttx_count: number;\n\tevent: ReturnType<typeof createHash>;\n\tevent_count: number;\n}\n\nexport class PerHeightDigestAccumulator {\n\tprivate readonly rows = new Map<number, Accumulator>();\n\n\tsetBlockDigest(height: number, blockDigestHex: string): void {\n\t\tthis.ensure(height).block_digest = blockDigestHex;\n\t}\n\n\tappendTransactionDigest(height: number, digestHex: string): void {\n\t\tconst a = this.ensure(height);\n\t\ta.tx.update(Buffer.from(digestHex, \"hex\"));\n\t\ta.tx_count += 1;\n\t}\n\n\tappendEventDigest(height: number, digestHex: string): void {\n\t\tconst a = this.ensure(height);\n\t\ta.event.update(Buffer.from(digestHex, \"hex\"));\n\t\ta.event_count += 1;\n\t}\n\n\t/**\n\t * Drain the accumulator into height-ascending rows. Heights with no block\n\t * digest are dropped with a thrown error — a canonical partition MUST have\n\t * a block at every height it covers, and silently emitting a row without\n\t * one would hide the missing block from downstream verifiers.\n\t */\n\tdrain(fromBlock: number, toBlock: number): PerHeightDigestRow[] {\n\t\tconst rows: PerHeightDigestRow[] = [];\n\t\tfor (let h = fromBlock; h <= toBlock; h++) {\n\t\t\tconst a = this.rows.get(h);\n\t\t\tif (!a) continue; // Missing entirely — surfaced by canonical audit, not here.\n\t\t\tif (!a.block_digest) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`per-height digest at ${h} has tx/event rollups but no block digest`,\n\t\t\t\t);\n\t\t\t}\n\t\t\trows.push({\n\t\t\t\theight: h,\n\t\t\t\tblock_digest: a.block_digest,\n\t\t\t\ttransactions_rollup:\n\t\t\t\t\ta.tx_count === 0 ? null : a.tx.copy().digest(\"hex\"),\n\t\t\t\tevents_rollup:\n\t\t\t\t\ta.event_count === 0 ? null : a.event.copy().digest(\"hex\"),\n\t\t\t});\n\t\t}\n\t\treturn rows;\n\t}\n\n\tspec(): typeof SEMANTIC_DIGEST_SPEC_V1 {\n\t\treturn SEMANTIC_DIGEST_SPEC_V1;\n\t}\n\n\tprivate ensure(height: number): Accumulator {\n\t\tlet a = this.rows.get(height);\n\t\tif (!a) {\n\t\t\ta = {\n\t\t\t\tblock_digest: null,\n\t\t\t\ttx: createHash(\"sha256\"),\n\t\t\t\ttx_count: 0,\n\t\t\t\tevent: createHash(\"sha256\"),\n\t\t\t\tevent_count: 0,\n\t\t\t};\n\t\t\tthis.rows.set(height, a);\n\t\t}\n\t\treturn a;\n\t}\n}\n\n/**\n * Registered in the snapshot manifest under `digest_index[]`. One entry per\n * partition, addressable at height granularity without downloading raw data.\n */\nexport interface DigestIndexPartition {\n\tfrom_block: number;\n\tto_block: number;\n\tpath: string;\n\trow_count: number;\n\tbyte_size: number;\n\tsha256: string;\n\tdigest_spec: typeof SEMANTIC_DIGEST_SPEC_V1;\n}\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "2ABEAEFAD6B6D8C464756E2164756E21",
10
+ "names": []
11
+ }