@secondlayer/shared 13.13.0 → 13.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/archive/copy-loader.d.ts +11 -12
- package/dist/src/archive/copy-loader.js +100 -39
- package/dist/src/archive/copy-loader.js.map +5 -4
- package/dist/src/archive/datasets.d.ts +37 -0
- package/dist/src/archive/datasets.js +50 -0
- package/dist/src/archive/datasets.js.map +10 -0
- package/dist/src/archive/derived-stage-report.d.ts +12 -26
- package/dist/src/archive/derived-stage-report.js +42 -6
- package/dist/src/archive/derived-stage-report.js.map +5 -4
- package/dist/src/archive/per-height-digest.d.ts +1 -0
- package/dist/src/archive/per-height-digest.js +13 -2
- package/dist/src/archive/per-height-digest.js.map +3 -3
- package/dist/src/archive/range-digest.d.ts +16 -1
- package/dist/src/archive/range-digest.js +107 -41
- package/dist/src/archive/range-digest.js.map +5 -4
- package/dist/src/archive/reference.d.ts +18 -2
- package/dist/src/archive/semantic-digest-builder.d.ts +30 -3
- package/dist/src/archive/semantic-digest-builder.js +143 -39
- package/dist/src/archive/semantic-digest-builder.js.map +6 -5
- package/dist/src/archive/semantic-digest.d.ts +29 -3
- package/dist/src/archive/semantic-digest.js +13 -2
- package/dist/src/archive/semantic-digest.js.map +3 -3
- package/dist/src/coverage/index.d.ts +0 -4
- package/dist/src/coverage/index.js +42 -2
- package/dist/src/coverage/index.js.map +5 -4
- package/dist/src/email/index.d.ts +27 -6
- package/dist/src/email/index.js +135 -29
- package/dist/src/email/index.js.map +3 -3
- package/package.json +5 -1
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { Writable } from "node:stream";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* The canonical archive's datasets — the one list every exporter, loader,
|
|
4
|
+
* verifier and fetch gate derives from.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* COPY moves the same data at ~64k rows/s because it is one statement per
|
|
8
|
-
* partition with no per-row round-trip.
|
|
6
|
+
* Order is load order: each dataset's foreign keys point only at datasets
|
|
7
|
+
* earlier in the list, so loading in this order never violates a constraint.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* two subtly different CSV encoders.
|
|
9
|
+
* Per-dataset code branches with an exhaustive `switch` ending in
|
|
10
|
+
* `assertNeverDataset`, never a trailing default: adding a dataset here must
|
|
11
|
+
* fail to compile everywhere it needs its own schema, SQL or table, instead of
|
|
12
|
+
* silently inheriting another dataset's.
|
|
15
13
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
declare const ARCHIVE_DATASETS: readonly ["blocks", "transactions", "events", "state_writes"];
|
|
15
|
+
/** One archive dataset: blocks, transactions, events or state writes. */
|
|
16
|
+
type ArchiveDataset = (typeof ARCHIVE_DATASETS)[number];
|
|
18
17
|
/** Row shapes exactly as the archive's Parquet columns are named. */
|
|
19
18
|
type ArchiveRow = Record<string, unknown>;
|
|
20
19
|
/**
|
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
// src/archive/copy-loader.ts
|
|
2
2
|
import { once } from "node:events";
|
|
3
|
+
|
|
4
|
+
// src/archive/datasets.ts
|
|
5
|
+
var ARCHIVE_DATASETS = [
|
|
6
|
+
"blocks",
|
|
7
|
+
"transactions",
|
|
8
|
+
"events",
|
|
9
|
+
"state_writes"
|
|
10
|
+
];
|
|
11
|
+
function isArchiveDataset(value) {
|
|
12
|
+
return ARCHIVE_DATASETS.includes(value);
|
|
13
|
+
}
|
|
14
|
+
var OPTIONAL_DATASET = {
|
|
15
|
+
blocks: false,
|
|
16
|
+
transactions: false,
|
|
17
|
+
events: false,
|
|
18
|
+
state_writes: true
|
|
19
|
+
};
|
|
20
|
+
function isOptionalDataset(dataset) {
|
|
21
|
+
return OPTIONAL_DATASET[dataset];
|
|
22
|
+
}
|
|
23
|
+
function manifestPublishes(manifest, dataset) {
|
|
24
|
+
if (!isOptionalDataset(dataset))
|
|
25
|
+
return true;
|
|
26
|
+
return (manifest.partitions ?? []).some((p) => p.dataset === dataset) || (manifest.zero_record_ranges ?? []).some((z) => z.dataset === dataset);
|
|
27
|
+
}
|
|
28
|
+
function archiveHeightColumn(dataset) {
|
|
29
|
+
switch (dataset) {
|
|
30
|
+
case "blocks":
|
|
31
|
+
return "height";
|
|
32
|
+
case "transactions":
|
|
33
|
+
case "events":
|
|
34
|
+
case "state_writes":
|
|
35
|
+
return "block_height";
|
|
36
|
+
default:
|
|
37
|
+
return assertNeverDataset(dataset);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function assertNeverDataset(dataset) {
|
|
41
|
+
throw new Error(`unhandled archive dataset: ${String(dataset)}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/archive/copy-loader.ts
|
|
3
45
|
function csvField(value) {
|
|
4
46
|
if (value === null || value === undefined)
|
|
5
47
|
return "\\N";
|
|
@@ -9,6 +51,9 @@ function csvField(value) {
|
|
|
9
51
|
function asNumber(value) {
|
|
10
52
|
return typeof value === "bigint" ? Number(value) : Number(value);
|
|
11
53
|
}
|
|
54
|
+
function asOptionalNumber(value) {
|
|
55
|
+
return value === null || value === undefined ? null : asNumber(value);
|
|
56
|
+
}
|
|
12
57
|
function asText(value) {
|
|
13
58
|
return Buffer.isBuffer(value) ? value.toString("utf8") : String(value);
|
|
14
59
|
}
|
|
@@ -16,48 +61,64 @@ function asOptionalText(value) {
|
|
|
16
61
|
return value === null || value === undefined ? null : asText(value);
|
|
17
62
|
}
|
|
18
63
|
function toCsvLine(dataset, row) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
64
|
+
switch (dataset) {
|
|
65
|
+
case "blocks":
|
|
66
|
+
return [
|
|
67
|
+
csvField(asNumber(row.height)),
|
|
68
|
+
csvField(asText(row.hash)),
|
|
69
|
+
csvField(asText(row.parent_hash)),
|
|
70
|
+
csvField(asNumber(row.burn_block_height)),
|
|
71
|
+
csvField(asOptionalText(row.burn_block_hash)),
|
|
72
|
+
csvField(asOptionalText(row.index_block_hash)),
|
|
73
|
+
csvField(asNumber(row.timestamp))
|
|
74
|
+
].join(",");
|
|
75
|
+
case "transactions":
|
|
76
|
+
return [
|
|
77
|
+
csvField(asText(row.tx_id)),
|
|
78
|
+
csvField(asNumber(row.block_height)),
|
|
79
|
+
csvField(asNumber(row.tx_index)),
|
|
80
|
+
csvField(asText(row.type)),
|
|
81
|
+
csvField(asText(row.sender)),
|
|
82
|
+
csvField(asText(row.status)),
|
|
83
|
+
csvField(asOptionalText(row.contract_id)),
|
|
84
|
+
csvField(asOptionalText(row.function_name)),
|
|
85
|
+
csvField(asOptionalText(row.function_args_json)),
|
|
86
|
+
csvField(asOptionalText(row.raw_result)),
|
|
87
|
+
csvField(asText(row.raw_tx))
|
|
88
|
+
].join(",");
|
|
89
|
+
case "events":
|
|
90
|
+
return [
|
|
91
|
+
csvField(asText(row.tx_id)),
|
|
92
|
+
csvField(asNumber(row.block_height)),
|
|
93
|
+
csvField(asNumber(row.event_index)),
|
|
94
|
+
csvField(asText(row.event_type)),
|
|
95
|
+
csvField(asText(row.data_json))
|
|
96
|
+
].join(",");
|
|
97
|
+
case "state_writes":
|
|
98
|
+
return [
|
|
99
|
+
csvField(asNumber(row.block_height)),
|
|
100
|
+
csvField(asNumber(row.ordinal)),
|
|
101
|
+
csvField(asOptionalNumber(row.tx_index)),
|
|
102
|
+
csvField(asText(row.key)),
|
|
103
|
+
csvField(asText(row.value_hex))
|
|
104
|
+
].join(",");
|
|
105
|
+
default:
|
|
106
|
+
return assertNeverDataset(dataset);
|
|
44
107
|
}
|
|
45
|
-
return [
|
|
46
|
-
csvField(asText(row.tx_id)),
|
|
47
|
-
csvField(asNumber(row.block_height)),
|
|
48
|
-
csvField(asNumber(row.event_index)),
|
|
49
|
-
csvField(asText(row.event_type)),
|
|
50
|
-
csvField(asText(row.data_json))
|
|
51
|
-
].join(",");
|
|
52
108
|
}
|
|
53
109
|
function copyStatement(dataset) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
110
|
+
switch (dataset) {
|
|
111
|
+
case "blocks":
|
|
112
|
+
return "COPY blocks (height, hash, parent_hash, burn_block_height, burn_block_hash, index_block_hash, timestamp) FROM STDIN WITH (FORMAT csv, NULL '\\N')";
|
|
113
|
+
case "transactions":
|
|
114
|
+
return "COPY transactions (tx_id, block_height, tx_index, type, sender, status, contract_id, function_name, function_args, raw_result, raw_tx) FROM STDIN WITH (FORMAT csv, NULL '\\N')";
|
|
115
|
+
case "events":
|
|
116
|
+
return "COPY events (tx_id, block_height, event_index, type, data) FROM STDIN WITH (FORMAT csv, NULL '\\N')";
|
|
117
|
+
case "state_writes":
|
|
118
|
+
return "COPY state_writes (block_height, ordinal, tx_index, key, value_hex) FROM STDIN WITH (FORMAT csv, NULL '\\N')";
|
|
119
|
+
default:
|
|
120
|
+
return assertNeverDataset(dataset);
|
|
59
121
|
}
|
|
60
|
-
return "COPY events (tx_id, block_height, event_index, type, data) FROM STDIN WITH (FORMAT csv, NULL '\\N')";
|
|
61
122
|
}
|
|
62
123
|
async function writeRowsToCopyStream(params) {
|
|
63
124
|
const { writable, dataset, rows } = params;
|
|
@@ -91,5 +152,5 @@ export {
|
|
|
91
152
|
writeRowsToCopyStream
|
|
92
153
|
};
|
|
93
154
|
|
|
94
|
-
//# debugId=
|
|
155
|
+
//# debugId=63CC074406EEB2EC64756E2164756E21
|
|
95
156
|
//# sourceMappingURL=copy-loader.js.map
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/archive/copy-loader.ts"],
|
|
3
|
+
"sources": ["../src/archive/copy-loader.ts", "../src/archive/datasets.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import { once } from \"node:events\";\nimport type { Writable } from \"node:stream\";\n\n/**\n * Streaming `COPY ... FROM STDIN` loader for canonical archive rows.\n *\n * Restoring history row-by-row is not viable at chain scale: batched INSERTs\n * measured ~2k rows/s on this schema, which is days for a full genesis restore.\n * COPY moves the same data at ~64k rows/s because it is one statement per\n * partition with no per-row round-trip.\n *\n * This module deliberately knows nothing about Parquet or about where rows come\n * from — it accepts the archive's row shape as an async iterable. That keeps the\n * heavy file-format dependency out of every consumer that only needs to write\n * rows, and lets the CLI and the indexer share one loader rather than growing\n * two subtly different CSV encoders.\n */\n\
|
|
5
|
+
"import { once } from \"node:events\";\nimport type { Writable } from \"node:stream\";\nimport { type ArchiveDataset, assertNeverDataset } from \"./datasets.ts\";\n\n/**\n * Streaming `COPY ... FROM STDIN` loader for canonical archive rows.\n *\n * Restoring history row-by-row is not viable at chain scale: batched INSERTs\n * measured ~2k rows/s on this schema, which is days for a full genesis restore.\n * COPY moves the same data at ~64k rows/s because it is one statement per\n * partition with no per-row round-trip.\n *\n * This module deliberately knows nothing about Parquet or about where rows come\n * from — it accepts the archive's row shape as an async iterable. That keeps the\n * heavy file-format dependency out of every consumer that only needs to write\n * rows, and lets the CLI and the indexer share one loader rather than growing\n * two subtly different CSV encoders.\n */\n\nexport type { ArchiveDataset } from \"./datasets.ts\";\n\n/** Row shapes exactly as the archive's Parquet columns are named. */\nexport type ArchiveRow = Record<string, unknown>;\n\n/**\n * CSV encoding for `WITH (FORMAT csv, NULL '\\N')`: every present value is\n * quoted with embedded quotes doubled, so a value that happens to equal the\n * NULL marker cannot be misread as SQL NULL. Only a genuinely absent value is\n * written unquoted as `\\N`.\n */\nexport function csvField(value: string | number | null | undefined): string {\n\tif (value === null || value === undefined) return \"\\\\N\";\n\tconst text = typeof value === \"number\" ? String(value) : value;\n\treturn `\"${text.replace(/\"/g, '\"\"')}\"`;\n}\n\nfunction asNumber(value: unknown): number {\n\treturn typeof value === \"bigint\" ? Number(value) : Number(value as number);\n}\n\nfunction asOptionalNumber(value: unknown): number | null {\n\treturn value === null || value === undefined ? null : asNumber(value);\n}\n\nfunction asText(value: unknown): string {\n\treturn Buffer.isBuffer(value) ? value.toString(\"utf8\") : String(value);\n}\n\nfunction asOptionalText(value: unknown): string | null {\n\treturn value === null || value === undefined ? null : asText(value);\n}\n\nexport function toCsvLine(dataset: ArchiveDataset, row: ArchiveRow): string {\n\tswitch (dataset) {\n\t\tcase \"blocks\":\n\t\t\treturn [\n\t\t\t\tcsvField(asNumber(row.height)),\n\t\t\t\tcsvField(asText(row.hash)),\n\t\t\t\tcsvField(asText(row.parent_hash)),\n\t\t\t\tcsvField(asNumber(row.burn_block_height)),\n\t\t\t\tcsvField(asOptionalText(row.burn_block_hash)),\n\t\t\t\tcsvField(asOptionalText(row.index_block_hash)),\n\t\t\t\tcsvField(asNumber(row.timestamp)),\n\t\t\t].join(\",\");\n\t\tcase \"transactions\":\n\t\t\treturn [\n\t\t\t\tcsvField(asText(row.tx_id)),\n\t\t\t\tcsvField(asNumber(row.block_height)),\n\t\t\t\tcsvField(asNumber(row.tx_index)),\n\t\t\t\tcsvField(asText(row.type)),\n\t\t\t\tcsvField(asText(row.sender)),\n\t\t\t\tcsvField(asText(row.status)),\n\t\t\t\tcsvField(asOptionalText(row.contract_id)),\n\t\t\t\tcsvField(asOptionalText(row.function_name)),\n\t\t\t\tcsvField(asOptionalText(row.function_args_json)),\n\t\t\t\tcsvField(asOptionalText(row.raw_result)),\n\t\t\t\tcsvField(asText(row.raw_tx)),\n\t\t\t].join(\",\");\n\t\tcase \"events\":\n\t\t\treturn [\n\t\t\t\tcsvField(asText(row.tx_id)),\n\t\t\t\tcsvField(asNumber(row.block_height)),\n\t\t\t\tcsvField(asNumber(row.event_index)),\n\t\t\t\tcsvField(asText(row.event_type)),\n\t\t\t\tcsvField(asText(row.data_json)),\n\t\t\t].join(\",\");\n\t\tcase \"state_writes\":\n\t\t\treturn [\n\t\t\t\tcsvField(asNumber(row.block_height)),\n\t\t\t\tcsvField(asNumber(row.ordinal)),\n\t\t\t\tcsvField(asOptionalNumber(row.tx_index)),\n\t\t\t\tcsvField(asText(row.key)),\n\t\t\t\tcsvField(asText(row.value_hex)),\n\t\t\t].join(\",\");\n\t\tdefault:\n\t\t\treturn assertNeverDataset(dataset);\n\t}\n}\n\n/**\n * `canonical` is omitted from the blocks column list: every archived block is\n * canonical by definition, and the schema defaults it to true, so carrying it\n * through every row would be dead weight.\n */\nexport function copyStatement(dataset: ArchiveDataset): string {\n\tswitch (dataset) {\n\t\tcase \"blocks\":\n\t\t\treturn \"COPY blocks (height, hash, parent_hash, burn_block_height, burn_block_hash, index_block_hash, timestamp) FROM STDIN WITH (FORMAT csv, NULL '\\\\N')\";\n\t\tcase \"transactions\":\n\t\t\treturn \"COPY transactions (tx_id, block_height, tx_index, type, sender, status, contract_id, function_name, function_args, raw_result, raw_tx) FROM STDIN WITH (FORMAT csv, NULL '\\\\N')\";\n\t\tcase \"events\":\n\t\t\treturn \"COPY events (tx_id, block_height, event_index, type, data) FROM STDIN WITH (FORMAT csv, NULL '\\\\N')\";\n\t\tcase \"state_writes\":\n\t\t\treturn \"COPY state_writes (block_height, ordinal, tx_index, key, value_hex) FROM STDIN WITH (FORMAT csv, NULL '\\\\N')\";\n\t\tdefault:\n\t\t\treturn assertNeverDataset(dataset);\n\t}\n}\n\n/**\n * Stream rows onto an open COPY writable. The caller owns opening it (drivers\n * differ in how a COPY stream is obtained) and this owns the wire format,\n * backpressure, and failure semantics.\n *\n * On error the stream is destroyed rather than ended, so Postgres aborts the\n * COPY instead of committing a partial one — an interrupted load must leave\n * nothing behind, not half a partition.\n */\nexport async function writeRowsToCopyStream(params: {\n\twritable: Writable;\n\tdataset: ArchiveDataset;\n\trows: AsyncIterable<ArchiveRow>;\n\tonProgress?: (rowsWritten: number) => void;\n\tprogressEvery?: number;\n}): Promise<number> {\n\tconst { writable, dataset, rows } = params;\n\tconst progressEvery = params.progressEvery ?? 500_000;\n\tlet count = 0;\n\ttry {\n\t\tfor await (const row of rows) {\n\t\t\tconst line = `${toCsvLine(dataset, row)}\\n`;\n\t\t\tif (!writable.write(line)) {\n\t\t\t\tawait once(writable, \"drain\");\n\t\t\t}\n\t\t\tcount++;\n\t\t\tif (params.onProgress && count % progressEvery === 0) {\n\t\t\t\tparams.onProgress(count);\n\t\t\t}\n\t\t}\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\twritable.end((error?: Error | null) =>\n\t\t\t\terror ? reject(error) : resolve(),\n\t\t\t);\n\t\t});\n\t} catch (error) {\n\t\twritable.destroy(error instanceof Error ? error : new Error(String(error)));\n\t\tthrow error;\n\t}\n\treturn count;\n}\n",
|
|
6
|
+
"/**\n * The canonical archive's datasets — the one list every exporter, loader,\n * verifier and fetch gate derives from.\n *\n * Order is load order: each dataset's foreign keys point only at datasets\n * earlier in the list, so loading in this order never violates a constraint.\n *\n * Per-dataset code branches with an exhaustive `switch` ending in\n * `assertNeverDataset`, never a trailing default: adding a dataset here must\n * fail to compile everywhere it needs its own schema, SQL or table, instead of\n * silently inheriting another dataset's.\n */\nexport const ARCHIVE_DATASETS = [\n\t\"blocks\",\n\t\"transactions\",\n\t\"events\",\n\t\"state_writes\",\n] as const;\n\n/** One archive dataset: blocks, transactions, events or state writes. */\nexport type ArchiveDataset = (typeof ARCHIVE_DATASETS)[number];\n\nexport function isArchiveDataset(value: string): value is ArchiveDataset {\n\treturn (ARCHIVE_DATASETS as readonly string[]).includes(value);\n}\n\n/**\n * Datasets a manifest may leave out. The exporter publishes `state_writes`\n * only from an instance that holds it from the start of the exported range,\n * so a manifest without it means \"not published\", never \"empty\": consumers\n * must not treat its absence as zero rows, or as rows missing locally.\n * The core datasets are always published; a manifest that lacks one of them\n * is incomplete, and consumers say so.\n */\nconst OPTIONAL_DATASET: Record<ArchiveDataset, boolean> = {\n\tblocks: false,\n\ttransactions: false,\n\tevents: false,\n\tstate_writes: true,\n};\n\nexport function isOptionalDataset(dataset: ArchiveDataset): boolean {\n\treturn OPTIONAL_DATASET[dataset];\n}\n\n/**\n * Whether a manifest publishes a dataset: always for a core dataset, and for\n * an optional one only when it names at least one partition or zero-record\n * range of it.\n */\nexport function manifestPublishes(\n\tmanifest: {\n\t\tpartitions?: ReadonlyArray<{ dataset: string }>;\n\t\tzero_record_ranges?: ReadonlyArray<{ dataset: string }>;\n\t},\n\tdataset: ArchiveDataset,\n): boolean {\n\tif (!isOptionalDataset(dataset)) return true;\n\treturn (\n\t\t(manifest.partitions ?? []).some((p) => p.dataset === dataset) ||\n\t\t(manifest.zero_record_ranges ?? []).some((z) => z.dataset === dataset)\n\t);\n}\n\n/**\n * The column a dataset's rows are placed at a height by. Each dataset is\n * stored in the table of the same name.\n */\nexport function archiveHeightColumn(\n\tdataset: ArchiveDataset,\n): \"height\" | \"block_height\" {\n\tswitch (dataset) {\n\t\tcase \"blocks\":\n\t\t\treturn \"height\";\n\t\tcase \"transactions\":\n\t\tcase \"events\":\n\t\tcase \"state_writes\":\n\t\t\treturn \"block_height\";\n\t\tdefault:\n\t\t\treturn assertNeverDataset(dataset);\n\t}\n}\n\nexport function assertNeverDataset(dataset: never): never {\n\tthrow new Error(`unhandled archive dataset: ${String(dataset)}`);\n}\n"
|
|
6
7
|
],
|
|
7
|
-
"mappings": ";AAAA;
|
|
8
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "63CC074406EEB2EC64756E2164756E21",
|
|
9
10
|
"names": []
|
|
10
11
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical archive's datasets — the one list every exporter, loader,
|
|
3
|
+
* verifier and fetch gate derives from.
|
|
4
|
+
*
|
|
5
|
+
* Order is load order: each dataset's foreign keys point only at datasets
|
|
6
|
+
* earlier in the list, so loading in this order never violates a constraint.
|
|
7
|
+
*
|
|
8
|
+
* Per-dataset code branches with an exhaustive `switch` ending in
|
|
9
|
+
* `assertNeverDataset`, never a trailing default: adding a dataset here must
|
|
10
|
+
* fail to compile everywhere it needs its own schema, SQL or table, instead of
|
|
11
|
+
* silently inheriting another dataset's.
|
|
12
|
+
*/
|
|
13
|
+
declare const ARCHIVE_DATASETS: readonly ["blocks", "transactions", "events", "state_writes"];
|
|
14
|
+
/** One archive dataset: blocks, transactions, events or state writes. */
|
|
15
|
+
type ArchiveDataset = (typeof ARCHIVE_DATASETS)[number];
|
|
16
|
+
declare function isArchiveDataset(value: string): value is ArchiveDataset;
|
|
17
|
+
declare function isOptionalDataset(dataset: ArchiveDataset): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Whether a manifest publishes a dataset: always for a core dataset, and for
|
|
20
|
+
* an optional one only when it names at least one partition or zero-record
|
|
21
|
+
* range of it.
|
|
22
|
+
*/
|
|
23
|
+
declare function manifestPublishes(manifest: {
|
|
24
|
+
partitions?: ReadonlyArray<{
|
|
25
|
+
dataset: string
|
|
26
|
+
}>
|
|
27
|
+
zero_record_ranges?: ReadonlyArray<{
|
|
28
|
+
dataset: string
|
|
29
|
+
}>
|
|
30
|
+
}, dataset: ArchiveDataset): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The column a dataset's rows are placed at a height by. Each dataset is
|
|
33
|
+
* stored in the table of the same name.
|
|
34
|
+
*/
|
|
35
|
+
declare function archiveHeightColumn(dataset: ArchiveDataset): "height" | "block_height";
|
|
36
|
+
declare function assertNeverDataset(dataset: never): never;
|
|
37
|
+
export { ARCHIVE_DATASETS, ArchiveDataset, archiveHeightColumn, assertNeverDataset, isArchiveDataset, isOptionalDataset, manifestPublishes };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/archive/datasets.ts
|
|
2
|
+
var ARCHIVE_DATASETS = [
|
|
3
|
+
"blocks",
|
|
4
|
+
"transactions",
|
|
5
|
+
"events",
|
|
6
|
+
"state_writes"
|
|
7
|
+
];
|
|
8
|
+
function isArchiveDataset(value) {
|
|
9
|
+
return ARCHIVE_DATASETS.includes(value);
|
|
10
|
+
}
|
|
11
|
+
var OPTIONAL_DATASET = {
|
|
12
|
+
blocks: false,
|
|
13
|
+
transactions: false,
|
|
14
|
+
events: false,
|
|
15
|
+
state_writes: true
|
|
16
|
+
};
|
|
17
|
+
function isOptionalDataset(dataset) {
|
|
18
|
+
return OPTIONAL_DATASET[dataset];
|
|
19
|
+
}
|
|
20
|
+
function manifestPublishes(manifest, dataset) {
|
|
21
|
+
if (!isOptionalDataset(dataset))
|
|
22
|
+
return true;
|
|
23
|
+
return (manifest.partitions ?? []).some((p) => p.dataset === dataset) || (manifest.zero_record_ranges ?? []).some((z) => z.dataset === dataset);
|
|
24
|
+
}
|
|
25
|
+
function archiveHeightColumn(dataset) {
|
|
26
|
+
switch (dataset) {
|
|
27
|
+
case "blocks":
|
|
28
|
+
return "height";
|
|
29
|
+
case "transactions":
|
|
30
|
+
case "events":
|
|
31
|
+
case "state_writes":
|
|
32
|
+
return "block_height";
|
|
33
|
+
default:
|
|
34
|
+
return assertNeverDataset(dataset);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function assertNeverDataset(dataset) {
|
|
38
|
+
throw new Error(`unhandled archive dataset: ${String(dataset)}`);
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
ARCHIVE_DATASETS,
|
|
42
|
+
archiveHeightColumn,
|
|
43
|
+
assertNeverDataset,
|
|
44
|
+
isArchiveDataset,
|
|
45
|
+
isOptionalDataset,
|
|
46
|
+
manifestPublishes
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
//# debugId=EF9D77F8C69ED8D264756E2164756E21
|
|
50
|
+
//# sourceMappingURL=datasets.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/archive/datasets.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * The canonical archive's datasets — the one list every exporter, loader,\n * verifier and fetch gate derives from.\n *\n * Order is load order: each dataset's foreign keys point only at datasets\n * earlier in the list, so loading in this order never violates a constraint.\n *\n * Per-dataset code branches with an exhaustive `switch` ending in\n * `assertNeverDataset`, never a trailing default: adding a dataset here must\n * fail to compile everywhere it needs its own schema, SQL or table, instead of\n * silently inheriting another dataset's.\n */\nexport const ARCHIVE_DATASETS = [\n\t\"blocks\",\n\t\"transactions\",\n\t\"events\",\n\t\"state_writes\",\n] as const;\n\n/** One archive dataset: blocks, transactions, events or state writes. */\nexport type ArchiveDataset = (typeof ARCHIVE_DATASETS)[number];\n\nexport function isArchiveDataset(value: string): value is ArchiveDataset {\n\treturn (ARCHIVE_DATASETS as readonly string[]).includes(value);\n}\n\n/**\n * Datasets a manifest may leave out. The exporter publishes `state_writes`\n * only from an instance that holds it from the start of the exported range,\n * so a manifest without it means \"not published\", never \"empty\": consumers\n * must not treat its absence as zero rows, or as rows missing locally.\n * The core datasets are always published; a manifest that lacks one of them\n * is incomplete, and consumers say so.\n */\nconst OPTIONAL_DATASET: Record<ArchiveDataset, boolean> = {\n\tblocks: false,\n\ttransactions: false,\n\tevents: false,\n\tstate_writes: true,\n};\n\nexport function isOptionalDataset(dataset: ArchiveDataset): boolean {\n\treturn OPTIONAL_DATASET[dataset];\n}\n\n/**\n * Whether a manifest publishes a dataset: always for a core dataset, and for\n * an optional one only when it names at least one partition or zero-record\n * range of it.\n */\nexport function manifestPublishes(\n\tmanifest: {\n\t\tpartitions?: ReadonlyArray<{ dataset: string }>;\n\t\tzero_record_ranges?: ReadonlyArray<{ dataset: string }>;\n\t},\n\tdataset: ArchiveDataset,\n): boolean {\n\tif (!isOptionalDataset(dataset)) return true;\n\treturn (\n\t\t(manifest.partitions ?? []).some((p) => p.dataset === dataset) ||\n\t\t(manifest.zero_record_ranges ?? []).some((z) => z.dataset === dataset)\n\t);\n}\n\n/**\n * The column a dataset's rows are placed at a height by. Each dataset is\n * stored in the table of the same name.\n */\nexport function archiveHeightColumn(\n\tdataset: ArchiveDataset,\n): \"height\" | \"block_height\" {\n\tswitch (dataset) {\n\t\tcase \"blocks\":\n\t\t\treturn \"height\";\n\t\tcase \"transactions\":\n\t\tcase \"events\":\n\t\tcase \"state_writes\":\n\t\t\treturn \"block_height\";\n\t\tdefault:\n\t\t\treturn assertNeverDataset(dataset);\n\t}\n}\n\nexport function assertNeverDataset(dataset: never): never {\n\tthrow new Error(`unhandled archive dataset: ${String(dataset)}`);\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";AAYO,IAAM,mBAAmB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD;AAKO,SAAS,gBAAgB,CAAC,OAAwC;AAAA,EACxE,OAAQ,iBAAuC,SAAS,KAAK;AAAA;AAW9D,IAAM,mBAAoD;AAAA,EACzD,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,cAAc;AACf;AAEO,SAAS,iBAAiB,CAAC,SAAkC;AAAA,EACnE,OAAO,iBAAiB;AAAA;AAQlB,SAAS,iBAAiB,CAChC,UAIA,SACU;AAAA,EACV,IAAI,CAAC,kBAAkB,OAAO;AAAA,IAAG,OAAO;AAAA,EACxC,QACE,SAAS,cAAc,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,YAAY,OAAO,MAC5D,SAAS,sBAAsB,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,YAAY,OAAO;AAAA;AAQhE,SAAS,mBAAmB,CAClC,SAC4B;AAAA,EAC5B,QAAQ;AAAA,SACF;AAAA,MACJ,OAAO;AAAA,SACH;AAAA,SACA;AAAA,SACA;AAAA,MACJ,OAAO;AAAA;AAAA,MAEP,OAAO,mBAAmB,OAAO;AAAA;AAAA;AAI7B,SAAS,kBAAkB,CAAC,SAAuB;AAAA,EACzD,MAAM,IAAI,MAAM,8BAA8B,OAAO,OAAO,GAAG;AAAA;",
|
|
8
|
+
"debugId": "EF9D77F8C69ED8D264756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -1,35 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* The canonical archive's datasets — the one list every exporter, loader,
|
|
3
|
+
* verifier and fetch gate derives from.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* (decoders, subgraphs, webhook 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.
|
|
5
|
+
* Order is load order: each dataset's foreign keys point only at datasets
|
|
6
|
+
* earlier in the list, so loading in this order never violates a constraint.
|
|
11
7
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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.
|
|
8
|
+
* Per-dataset code branches with an exhaustive `switch` ending in
|
|
9
|
+
* `assertNeverDataset`, never a trailing default: adding a dataset here must
|
|
10
|
+
* fail to compile everywhere it needs its own schema, SQL or table, instead of
|
|
11
|
+
* silently inheriting another dataset's.
|
|
29
12
|
*/
|
|
13
|
+
declare const ARCHIVE_DATASETS: readonly ["blocks", "transactions", "events", "state_writes"];
|
|
14
|
+
/** One archive dataset: blocks, transactions, events or state writes. */
|
|
15
|
+
type ArchiveDataset = (typeof ARCHIVE_DATASETS)[number];
|
|
30
16
|
declare const DERIVED_STAGE_REPORT_SCHEMA_VERSION: 1;
|
|
31
17
|
type ServiceKind = "decoder" | "subgraph-runtime" | "webhook-runtime" | "protocol-producer";
|
|
32
|
-
type CanonicalInput =
|
|
18
|
+
type CanonicalInput = ArchiveDataset;
|
|
33
19
|
interface ExternalInput {
|
|
34
20
|
/** Short name for the dependency: `stacks-node-rpc`, `hiro-extended-api`,
|
|
35
21
|
* `observer-journal`, `contract-source-registry`, etc. */
|
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
// src/archive/datasets.ts
|
|
2
|
+
var ARCHIVE_DATASETS = [
|
|
3
|
+
"blocks",
|
|
4
|
+
"transactions",
|
|
5
|
+
"events",
|
|
6
|
+
"state_writes"
|
|
7
|
+
];
|
|
8
|
+
function isArchiveDataset(value) {
|
|
9
|
+
return ARCHIVE_DATASETS.includes(value);
|
|
10
|
+
}
|
|
11
|
+
var OPTIONAL_DATASET = {
|
|
12
|
+
blocks: false,
|
|
13
|
+
transactions: false,
|
|
14
|
+
events: false,
|
|
15
|
+
state_writes: true
|
|
16
|
+
};
|
|
17
|
+
function isOptionalDataset(dataset) {
|
|
18
|
+
return OPTIONAL_DATASET[dataset];
|
|
19
|
+
}
|
|
20
|
+
function manifestPublishes(manifest, dataset) {
|
|
21
|
+
if (!isOptionalDataset(dataset))
|
|
22
|
+
return true;
|
|
23
|
+
return (manifest.partitions ?? []).some((p) => p.dataset === dataset) || (manifest.zero_record_ranges ?? []).some((z) => z.dataset === dataset);
|
|
24
|
+
}
|
|
25
|
+
function archiveHeightColumn(dataset) {
|
|
26
|
+
switch (dataset) {
|
|
27
|
+
case "blocks":
|
|
28
|
+
return "height";
|
|
29
|
+
case "transactions":
|
|
30
|
+
case "events":
|
|
31
|
+
case "state_writes":
|
|
32
|
+
return "block_height";
|
|
33
|
+
default:
|
|
34
|
+
return assertNeverDataset(dataset);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function assertNeverDataset(dataset) {
|
|
38
|
+
throw new Error(`unhandled archive dataset: ${String(dataset)}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
1
41
|
// src/archive/derived-stage-report.ts
|
|
2
42
|
var DERIVED_STAGE_REPORT_SCHEMA_VERSION = 1;
|
|
3
43
|
function buildDerivedStageReport(manifests, generatedAt) {
|
|
@@ -11,11 +51,7 @@ function buildDerivedStageReport(manifests, generatedAt) {
|
|
|
11
51
|
function validateServiceManifests(manifests) {
|
|
12
52
|
const issues = [];
|
|
13
53
|
const seen = new Set;
|
|
14
|
-
const canonicalDatasets = new Set(
|
|
15
|
-
"blocks",
|
|
16
|
-
"transactions",
|
|
17
|
-
"events"
|
|
18
|
-
]);
|
|
54
|
+
const canonicalDatasets = new Set(ARCHIVE_DATASETS);
|
|
19
55
|
for (const m of manifests) {
|
|
20
56
|
if (seen.has(m.name)) {
|
|
21
57
|
issues.push({ name: m.name, problem: "duplicate service name" });
|
|
@@ -44,5 +80,5 @@ export {
|
|
|
44
80
|
validateServiceManifests
|
|
45
81
|
};
|
|
46
82
|
|
|
47
|
-
//# debugId=
|
|
83
|
+
//# debugId=4E289EF15D65D38E64756E2164756E21
|
|
48
84
|
//# sourceMappingURL=derived-stage-report.js.map
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/archive/derived-stage-report.ts"],
|
|
3
|
+
"sources": ["../src/archive/datasets.ts", "../src/archive/derived-stage-report.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n *
|
|
5
|
+
"/**\n * The canonical archive's datasets — the one list every exporter, loader,\n * verifier and fetch gate derives from.\n *\n * Order is load order: each dataset's foreign keys point only at datasets\n * earlier in the list, so loading in this order never violates a constraint.\n *\n * Per-dataset code branches with an exhaustive `switch` ending in\n * `assertNeverDataset`, never a trailing default: adding a dataset here must\n * fail to compile everywhere it needs its own schema, SQL or table, instead of\n * silently inheriting another dataset's.\n */\nexport const ARCHIVE_DATASETS = [\n\t\"blocks\",\n\t\"transactions\",\n\t\"events\",\n\t\"state_writes\",\n] as const;\n\n/** One archive dataset: blocks, transactions, events or state writes. */\nexport type ArchiveDataset = (typeof ARCHIVE_DATASETS)[number];\n\nexport function isArchiveDataset(value: string): value is ArchiveDataset {\n\treturn (ARCHIVE_DATASETS as readonly string[]).includes(value);\n}\n\n/**\n * Datasets a manifest may leave out. The exporter publishes `state_writes`\n * only from an instance that holds it from the start of the exported range,\n * so a manifest without it means \"not published\", never \"empty\": consumers\n * must not treat its absence as zero rows, or as rows missing locally.\n * The core datasets are always published; a manifest that lacks one of them\n * is incomplete, and consumers say so.\n */\nconst OPTIONAL_DATASET: Record<ArchiveDataset, boolean> = {\n\tblocks: false,\n\ttransactions: false,\n\tevents: false,\n\tstate_writes: true,\n};\n\nexport function isOptionalDataset(dataset: ArchiveDataset): boolean {\n\treturn OPTIONAL_DATASET[dataset];\n}\n\n/**\n * Whether a manifest publishes a dataset: always for a core dataset, and for\n * an optional one only when it names at least one partition or zero-record\n * range of it.\n */\nexport function manifestPublishes(\n\tmanifest: {\n\t\tpartitions?: ReadonlyArray<{ dataset: string }>;\n\t\tzero_record_ranges?: ReadonlyArray<{ dataset: string }>;\n\t},\n\tdataset: ArchiveDataset,\n): boolean {\n\tif (!isOptionalDataset(dataset)) return true;\n\treturn (\n\t\t(manifest.partitions ?? []).some((p) => p.dataset === dataset) ||\n\t\t(manifest.zero_record_ranges ?? []).some((z) => z.dataset === dataset)\n\t);\n}\n\n/**\n * The column a dataset's rows are placed at a height by. Each dataset is\n * stored in the table of the same name.\n */\nexport function archiveHeightColumn(\n\tdataset: ArchiveDataset,\n): \"height\" | \"block_height\" {\n\tswitch (dataset) {\n\t\tcase \"blocks\":\n\t\t\treturn \"height\";\n\t\tcase \"transactions\":\n\t\tcase \"events\":\n\t\tcase \"state_writes\":\n\t\t\treturn \"block_height\";\n\t\tdefault:\n\t\t\treturn assertNeverDataset(dataset);\n\t}\n}\n\nexport function assertNeverDataset(dataset: never): never {\n\tthrow new Error(`unhandled archive dataset: ${String(dataset)}`);\n}\n",
|
|
6
|
+
"/**\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, webhook 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\nimport { ARCHIVE_DATASETS, type ArchiveDataset } from \"./datasets.ts\";\n\nexport const DERIVED_STAGE_REPORT_SCHEMA_VERSION = 1 as const;\n\nexport type ServiceKind =\n\t| \"decoder\"\n\t| \"subgraph-runtime\"\n\t| \"webhook-runtime\"\n\t| \"protocol-producer\";\n\nexport type CanonicalInput = ArchiveDataset;\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\tARCHIVE_DATASETS,\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
|
],
|
|
7
|
-
"mappings": ";
|
|
8
|
-
"debugId": "
|
|
8
|
+
"mappings": ";AAYO,IAAM,mBAAmB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD;AAKO,SAAS,gBAAgB,CAAC,OAAwC;AAAA,EACxE,OAAQ,iBAAuC,SAAS,KAAK;AAAA;AAW9D,IAAM,mBAAoD;AAAA,EACzD,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,cAAc;AACf;AAEO,SAAS,iBAAiB,CAAC,SAAkC;AAAA,EACnE,OAAO,iBAAiB;AAAA;AAQlB,SAAS,iBAAiB,CAChC,UAIA,SACU;AAAA,EACV,IAAI,CAAC,kBAAkB,OAAO;AAAA,IAAG,OAAO;AAAA,EACxC,QACE,SAAS,cAAc,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,YAAY,OAAO,MAC5D,SAAS,sBAAsB,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,YAAY,OAAO;AAAA;AAQhE,SAAS,mBAAmB,CAClC,SAC4B;AAAA,EAC5B,QAAQ;AAAA,SACF;AAAA,MACJ,OAAO;AAAA,SACH;AAAA,SACA;AAAA,SACA;AAAA,MACJ,OAAO;AAAA;AAAA,MAEP,OAAO,mBAAmB,OAAO;AAAA;AAAA;AAI7B,SAAS,kBAAkB,CAAC,SAAuB;AAAA,EACzD,MAAM,IAAI,MAAM,8BAA8B,OAAO,OAAO,GAAG;AAAA;;;ACpDzD,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,IAC1D,gBACD;AAAA,EACA,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;",
|
|
9
|
+
"debugId": "4E289EF15D65D38E64756E2164756E21",
|
|
9
10
|
"names": []
|
|
10
11
|
}
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
* - blocks: (height asc)
|
|
33
33
|
* - transactions: (block_height asc, tx_index asc, tx_id asc)
|
|
34
34
|
* - events: (block_height asc, event_index asc, tx_id asc)
|
|
35
|
+
* - state_writes: (block_height asc, ordinal asc)
|
|
35
36
|
* Per-partition rollup is `sha256(concat(row_digest_bytes))` in stream order.
|
|
36
37
|
*
|
|
37
38
|
* Test vectors under `packages/shared/src/archive/__fixtures__/semantic-digest/`
|
|
@@ -87,15 +87,26 @@ function encodeEvent(row) {
|
|
|
87
87
|
jsonScalar(row.data)
|
|
88
88
|
].join(FIELD_SEP);
|
|
89
89
|
}
|
|
90
|
+
function encodeStateWrite(row) {
|
|
91
|
+
return [
|
|
92
|
+
scalar(row.block_height),
|
|
93
|
+
scalar(row.ordinal),
|
|
94
|
+
scalar(row.tx_index),
|
|
95
|
+
scalar(row.key),
|
|
96
|
+
scalar(row.value_hex)
|
|
97
|
+
].join(FIELD_SEP);
|
|
98
|
+
}
|
|
90
99
|
var semanticDigest = {
|
|
91
100
|
v1: {
|
|
92
101
|
spec: SEMANTIC_DIGEST_SPEC_V1,
|
|
93
102
|
encodeBlock,
|
|
94
103
|
encodeTransaction,
|
|
95
104
|
encodeEvent,
|
|
105
|
+
encodeStateWrite,
|
|
96
106
|
block: (row) => sha256Hex(textEncoder.encode(encodeBlock(row))),
|
|
97
107
|
transaction: (row) => sha256Hex(textEncoder.encode(encodeTransaction(row))),
|
|
98
|
-
event: (row) => sha256Hex(textEncoder.encode(encodeEvent(row)))
|
|
108
|
+
event: (row) => sha256Hex(textEncoder.encode(encodeEvent(row))),
|
|
109
|
+
stateWrite: (row) => sha256Hex(textEncoder.encode(encodeStateWrite(row)))
|
|
99
110
|
}
|
|
100
111
|
};
|
|
101
112
|
|
|
@@ -176,5 +187,5 @@ export {
|
|
|
176
187
|
PerHeightDigestAccumulator
|
|
177
188
|
};
|
|
178
189
|
|
|
179
|
-
//# debugId=
|
|
190
|
+
//# debugId=93A25676A491D51564756E2164756E21
|
|
180
191
|
//# sourceMappingURL=per-height-digest.js.map
|