@sekiban/dcb-runtime 0.2.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 (153) hide show
  1. package/LICENSE +86 -0
  2. package/README.md +13 -0
  3. package/dist/allocator/AllocatorDurableObject.d.ts +36 -0
  4. package/dist/allocator/AllocatorDurableObject.js +658 -0
  5. package/dist/allocator/IssuanceLedger.d.ts +188 -0
  6. package/dist/allocator/IssuanceLedger.js +608 -0
  7. package/dist/allocator/IssuanceReconciler.d.ts +41 -0
  8. package/dist/allocator/IssuanceReconciler.js +191 -0
  9. package/dist/allocator/OrderClock.d.ts +41 -0
  10. package/dist/allocator/OrderClock.js +92 -0
  11. package/dist/allocator/SortableUniqueId.d.ts +46 -0
  12. package/dist/allocator/SortableUniqueId.js +156 -0
  13. package/dist/allocator/types.d.ts +52 -0
  14. package/dist/allocator/types.js +1 -0
  15. package/dist/bootstrap/BootstrapCoordinatorDurableObject.d.ts +28 -0
  16. package/dist/bootstrap/BootstrapCoordinatorDurableObject.js +357 -0
  17. package/dist/bootstrap/BootstrapStoreAdapter.d.ts +51 -0
  18. package/dist/bootstrap/BootstrapStoreAdapter.js +162 -0
  19. package/dist/bootstrap/OperatorBootstrap.d.ts +20 -0
  20. package/dist/bootstrap/OperatorBootstrap.js +104 -0
  21. package/dist/bootstrap/manifest.d.ts +9 -0
  22. package/dist/bootstrap/manifest.js +115 -0
  23. package/dist/bootstrap/types.d.ts +75 -0
  24. package/dist/bootstrap/types.js +1 -0
  25. package/dist/cli/OperatorRepairCli.d.ts +11 -0
  26. package/dist/cli/OperatorRepairCli.js +108 -0
  27. package/dist/cloudflare.d.ts +164 -0
  28. package/dist/cloudflare.js +19487 -0
  29. package/dist/commit/CommitWorker.d.ts +101 -0
  30. package/dist/commit/CommitWorker.js +921 -0
  31. package/dist/commit/types.d.ts +25 -0
  32. package/dist/commit/types.js +13 -0
  33. package/dist/completeness/GlobalCompletenessReconciler.d.ts +77 -0
  34. package/dist/completeness/GlobalCompletenessReconciler.js +572 -0
  35. package/dist/completeness/types.d.ts +85 -0
  36. package/dist/completeness/types.js +6 -0
  37. package/dist/composition.d.ts +123 -0
  38. package/dist/composition.js +237 -0
  39. package/dist/cosmos.d.ts +11 -0
  40. package/dist/cosmos.js +1411 -0
  41. package/dist/d1-mv.d.ts +8 -0
  42. package/dist/d1-mv.js +2001 -0
  43. package/dist/d1.d.ts +16 -0
  44. package/dist/d1.js +3418 -0
  45. package/dist/diagnostics/G60DurableHop.d.ts +99 -0
  46. package/dist/diagnostics/G60DurableHop.js +248 -0
  47. package/dist/diagnostics/G65Admission.d.ts +23 -0
  48. package/dist/diagnostics/G65Admission.js +43 -0
  49. package/dist/diagnostics/G65DirectRing.d.ts +33 -0
  50. package/dist/diagnostics/G65DirectRing.js +85 -0
  51. package/dist/diagnostics/G69AdmissionAttempt.d.ts +43 -0
  52. package/dist/diagnostics/G69AdmissionAttempt.js +22 -0
  53. package/dist/downstream/DeliveryCore.d.ts +127 -0
  54. package/dist/downstream/DeliveryCore.js +304 -0
  55. package/dist/downstream/Doorbell.d.ts +43 -0
  56. package/dist/downstream/Doorbell.js +137 -0
  57. package/dist/downstream/DownstreamAdapter.d.ts +52 -0
  58. package/dist/downstream/DownstreamAdapter.js +299 -0
  59. package/dist/downstream/ExclusionLookup.d.ts +36 -0
  60. package/dist/downstream/ExclusionLookup.js +50 -0
  61. package/dist/downstream/InconsistencyDetector.d.ts +27 -0
  62. package/dist/downstream/InconsistencyDetector.js +82 -0
  63. package/dist/downstream/OutboxDrain.d.ts +35 -0
  64. package/dist/downstream/OutboxDrain.js +126 -0
  65. package/dist/downstream/types.d.ts +77 -0
  66. package/dist/downstream/types.js +64 -0
  67. package/dist/eventIdentity.d.ts +30 -0
  68. package/dist/eventIdentity.js +49 -0
  69. package/dist/eventRecord.d.ts +26 -0
  70. package/dist/eventRecord.js +53 -0
  71. package/dist/g42-probe.d.ts +9 -0
  72. package/dist/g42-probe.js +9 -0
  73. package/dist/http/SerializedQueryWorker.d.ts +58 -0
  74. package/dist/http/SerializedQueryWorker.js +497 -0
  75. package/dist/http/commitResponse.d.ts +46 -0
  76. package/dist/http/commitResponse.js +80 -0
  77. package/dist/http/testServiceId.d.ts +17 -0
  78. package/dist/http/testServiceId.js +14 -0
  79. package/dist/index.d.ts +112 -0
  80. package/dist/index.js +19926 -0
  81. package/dist/journal/JournalDurableObject.d.ts +30 -0
  82. package/dist/journal/JournalDurableObject.js +481 -0
  83. package/dist/journal/JournalFirstTouchProbe.d.ts +190 -0
  84. package/dist/journal/JournalFirstTouchProbe.js +375 -0
  85. package/dist/journal/types.d.ts +116 -0
  86. package/dist/journal/types.js +52 -0
  87. package/dist/mv/MaterializedViewCatchUp.d.ts +60 -0
  88. package/dist/mv/MaterializedViewCatchUp.js +353 -0
  89. package/dist/mv/MaterializedViewRuntime.d.ts +80 -0
  90. package/dist/mv/MaterializedViewRuntime.js +328 -0
  91. package/dist/mv/MaterializedViewStore.d.ts +257 -0
  92. package/dist/mv/MaterializedViewStore.js +875 -0
  93. package/dist/mv/UnsafeWindowMaterializedView.d.ts +112 -0
  94. package/dist/mv/UnsafeWindowMaterializedView.js +495 -0
  95. package/dist/mv/index.d.ts +8 -0
  96. package/dist/mv/index.js +2357 -0
  97. package/dist/projection/LiveProjectionWorker.d.ts +86 -0
  98. package/dist/projection/LiveProjectionWorker.js +240 -0
  99. package/dist/projection/ProjectionRuntime.d.ts +113 -0
  100. package/dist/projection/ProjectionRuntime.js +346 -0
  101. package/dist/projection/ProjectorRegistry.d.ts +51 -0
  102. package/dist/projection/ProjectorRegistry.js +116 -0
  103. package/dist/query/ProjectionQueryStore.d.ts +102 -0
  104. package/dist/query/ProjectionQueryStore.js +185 -0
  105. package/dist/query/QueryRegistry.d.ts +22 -0
  106. package/dist/query/QueryRegistry.js +36 -0
  107. package/dist/read/SerializedReadWorker.d.ts +36 -0
  108. package/dist/read/SerializedReadWorker.js +225 -0
  109. package/dist/repair/RepairWorker.d.ts +72 -0
  110. package/dist/repair/RepairWorker.js +461 -0
  111. package/dist/safeWindow.d.ts +20 -0
  112. package/dist/safeWindow.js +36 -0
  113. package/dist/scope/ControlRouteScope.d.ts +22 -0
  114. package/dist/scope/ControlRouteScope.js +36 -0
  115. package/dist/scope/ScopeName.d.ts +34 -0
  116. package/dist/scope/ScopeName.js +67 -0
  117. package/dist/service/ServiceIdentityProvider.d.ts +35 -0
  118. package/dist/service/ServiceIdentityProvider.js +56 -0
  119. package/dist/store/CosmosEventStore.d.ts +126 -0
  120. package/dist/store/CosmosEventStore.js +950 -0
  121. package/dist/store/D1EventStore.d.ts +111 -0
  122. package/dist/store/D1EventStore.js +1111 -0
  123. package/dist/store/PostgresEventStore.d.ts +35 -0
  124. package/dist/store/PostgresEventStore.js +811 -0
  125. package/dist/store/provider.d.ts +25 -0
  126. package/dist/store/provider.js +32 -0
  127. package/dist/store/types.d.ts +205 -0
  128. package/dist/store/types.js +18 -0
  129. package/dist/tag/EventDigest.d.ts +40 -0
  130. package/dist/tag/EventDigest.js +158 -0
  131. package/dist/tag/TagDurableObject.d.ts +286 -0
  132. package/dist/tag/TagDurableObject.js +4071 -0
  133. package/dist/tag/TagSqlMeasurement.d.ts +26 -0
  134. package/dist/tag/TagSqlMeasurement.js +44 -0
  135. package/dist/tag/TagSqlSchema.d.ts +29 -0
  136. package/dist/tag/TagSqlSchema.js +211 -0
  137. package/dist/tag/types.d.ts +173 -0
  138. package/dist/tag/types.js +3 -0
  139. package/dist/tagstate/TagStateDurableObject.d.ts +57 -0
  140. package/dist/tagstate/TagStateDurableObject.js +499 -0
  141. package/dist/trace/AttributionRatio.d.ts +12 -0
  142. package/dist/trace/AttributionRatio.js +68 -0
  143. package/dist/trace/CloudflareTracing.d.ts +7 -0
  144. package/dist/trace/CloudflareTracing.js +9 -0
  145. package/dist/trace/CommitTrace.d.ts +405 -0
  146. package/dist/trace/CommitTrace.js +1008 -0
  147. package/dist/trace/CommitTraceConsoleSink.d.ts +68 -0
  148. package/dist/trace/CommitTraceConsoleSink.js +114 -0
  149. package/dist/trace/CommitTraceVerifier.d.ts +22 -0
  150. package/dist/trace/CommitTraceVerifier.js +295 -0
  151. package/dist/trace/ObservationStream.d.ts +118 -0
  152. package/dist/trace/ObservationStream.js +148 -0
  153. package/package.json +71 -0
@@ -0,0 +1,572 @@
1
+ import { scopeIdFor } from "../scope/ScopeName";
2
+ import { G44_SCANNER_VERSION, G44_HEALTH_STALE_AFTER_MS, } from "./types";
3
+ const PAGE_SIZE = 64;
4
+ function asString(value, name) {
5
+ if (typeof value !== "string" || value.length === 0)
6
+ throw new Error(`G44 ${name} must be a non-empty string`);
7
+ return value;
8
+ }
9
+ function asNumber(value, name) {
10
+ const number = typeof value === "number" ? value : Number(value);
11
+ if (!Number.isSafeInteger(number) || number < 0)
12
+ throw new Error(`G44 ${name} must be a non-negative safe integer`);
13
+ return number;
14
+ }
15
+ function errorText(error) {
16
+ return error instanceof Error && error.message.length > 0 ? error.message : String(error);
17
+ }
18
+ /**
19
+ * G44 originally stored only the scanner's vector snapshot in cursor_json.
20
+ * G58 deliberately keeps that historical shape readable while adding the
21
+ * exact high-water SUID that the FULL snapshot proved. A malformed or
22
+ * pre-G58 cursor never grants a frontier.
23
+ */
24
+ function settledFrontierFromCursor(cursorJson) {
25
+ const cursor = settledCursorFromJson(cursorJson);
26
+ return cursor?.frontierSuid ?? null;
27
+ }
28
+ function settledCursorFromJson(cursorJson) {
29
+ if (cursorJson === null)
30
+ return null;
31
+ try {
32
+ const parsed = JSON.parse(cursorJson);
33
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
34
+ return null;
35
+ const candidate = parsed;
36
+ if (candidate.schema !== "sdt-g58-settled-frontier/v1" || !Array.isArray(candidate.snapshots))
37
+ return null;
38
+ const snapshots = [];
39
+ for (const raw of candidate.snapshots) {
40
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
41
+ return null;
42
+ const snapshot = raw;
43
+ if (typeof snapshot.serviceId !== "string" || snapshot.serviceId.length === 0 ||
44
+ typeof snapshot.tag !== "string" || snapshot.tag.length === 0 ||
45
+ !Number.isSafeInteger(snapshot.upperBoundSequence) ||
46
+ snapshot.upperBoundSequence < 0)
47
+ return null;
48
+ snapshots.push({
49
+ serviceId: snapshot.serviceId,
50
+ tag: snapshot.tag,
51
+ upperBoundSequence: snapshot.upperBoundSequence,
52
+ });
53
+ }
54
+ const frontierSuid = candidate.frontierSuid;
55
+ if (frontierSuid !== null && (typeof frontierSuid !== "string" || frontierSuid.length === 0))
56
+ return null;
57
+ return {
58
+ schema: "sdt-g58-settled-frontier/v1",
59
+ snapshots,
60
+ frontierSuid: frontierSuid,
61
+ };
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ function cursorIncludesObligation(cursorJson, serviceId, tag, obligationSequence) {
68
+ const cursor = settledCursorFromJson(cursorJson);
69
+ return cursor !== null && cursor.snapshots.some((snapshot) => snapshot.serviceId === serviceId && snapshot.tag === tag && obligationSequence <= snapshot.upperBoundSequence);
70
+ }
71
+ function findingIdentity(serviceId, tag, obligation) {
72
+ return `SOURCE_RECEIPT_ABSENT|${serviceId}|${tag}|${obligation.obligationSequence}|${obligation.eventId}|${obligation.eventDigest}`;
73
+ }
74
+ function sourceUnavailableIdentity(serviceId, tag) {
75
+ return `SOURCE_PARTITION_UNAVAILABLE|${serviceId}|${tag}`;
76
+ }
77
+ function scannerFailureIdentity(serviceId) {
78
+ return `GLOBAL_SCANNER_FAILURE|${serviceId}`;
79
+ }
80
+ function isObligationStatus(value) {
81
+ return value === "pending" || value === "acknowledged" || value === "poison";
82
+ }
83
+ function sourcePageFrom(value) {
84
+ if (typeof value !== "object" || value === null || Array.isArray(value))
85
+ throw new Error("source_page_invalid_response");
86
+ const page = value;
87
+ if (!Array.isArray(page.rows))
88
+ throw new Error("source_page_invalid_rows");
89
+ const rows = page.rows.map((raw) => {
90
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
91
+ throw new Error("source_page_invalid_row");
92
+ const row = raw;
93
+ if (!Array.isArray(row.declaredTagSet) || !row.declaredTagSet.every((entry) => typeof entry === "string") ||
94
+ !Array.isArray(row.localCommittedMembership) || row.localCommittedMembership.length !== 1 ||
95
+ typeof row.canonicalBytesBase64 !== "string" || typeof row.eventDigest !== "string" ||
96
+ !/^[0-9a-f]{64}$/.test(row.eventDigest) || !isObligationStatus(row.status)) {
97
+ throw new Error("source_page_invalid_obligation_fact");
98
+ }
99
+ const membership = row.localCommittedMembership[0];
100
+ if (typeof membership !== "object" || membership === null || Array.isArray(membership))
101
+ throw new Error("source_page_invalid_membership");
102
+ const parsedMembership = membership;
103
+ return {
104
+ obligationSequence: asNumber(row.obligationSequence, "source_page.obligationSequence"),
105
+ eventId: asString(row.eventId, "source_page.eventId"),
106
+ eventDigest: asString(row.eventDigest, "source_page.eventDigest"),
107
+ canonicalBytesBase64: asString(row.canonicalBytesBase64, "source_page.canonicalBytesBase64"),
108
+ declaredTagSet: row.declaredTagSet,
109
+ localCommittedMembership: [{
110
+ serviceId: asString(parsedMembership.serviceId, "source_page.membership.serviceId"),
111
+ eventId: asString(parsedMembership.eventId, "source_page.membership.eventId"),
112
+ tag: asString(parsedMembership.tag, "source_page.membership.tag"),
113
+ }],
114
+ status: row.status,
115
+ };
116
+ });
117
+ if (typeof page.hasMore !== "boolean")
118
+ throw new Error("source_page.invalid_has_more");
119
+ return {
120
+ serviceId: asString(page.serviceId, "source_page.serviceId"),
121
+ tag: asString(page.tag, "source_page.tag"),
122
+ upperBoundSequence: asNumber(page.upperBoundSequence, "source_page.upperBoundSequence"),
123
+ observedMaxSequence: asNumber(page.observedMaxSequence, "source_page.observedMaxSequence"),
124
+ afterSequence: asNumber(page.afterSequence, "source_page.afterSequence"),
125
+ rows,
126
+ hasMore: page.hasMore,
127
+ };
128
+ }
129
+ /**
130
+ * An independent source→global reconciliation pass. It never receives a
131
+ * Queue message or an expected test count: its universe is the source-side
132
+ * partition registry captured before paging any Tag DO.
133
+ */
134
+ export class GlobalCompletenessReconciler {
135
+ database;
136
+ tags;
137
+ scannerVersion;
138
+ options;
139
+ constructor(database, tags, scannerVersion = G44_SCANNER_VERSION, options = {}) {
140
+ this.database = database;
141
+ this.tags = tags;
142
+ this.scannerVersion = scannerVersion;
143
+ this.options = options;
144
+ }
145
+ async reconcile(serviceId, nowMs) {
146
+ let snapshots;
147
+ try {
148
+ snapshots = await this.snapshotPartitions(serviceId);
149
+ }
150
+ catch (error) {
151
+ const message = errorText(error);
152
+ await this.appendFinding(serviceId, {
153
+ identity: scannerFailureIdentity(serviceId),
154
+ type: "GLOBAL_ARRAY_SCANNER_FAILURE",
155
+ partitionTag: null,
156
+ obligationSequence: null,
157
+ eventId: null,
158
+ eventDigest: null,
159
+ }, nowMs).catch(() => undefined);
160
+ await this.writeHealth(serviceId, "FAILED", null, null, message, nowMs).catch(() => undefined);
161
+ return { kind: "FAILED", error: message };
162
+ }
163
+ try {
164
+ let scanned = 0;
165
+ let findings = 0;
166
+ for (const snapshot of snapshots) {
167
+ try {
168
+ const source = this.tags.get(scopeIdFor(this.tags, {
169
+ serviceId: snapshot.serviceId,
170
+ doClass: "tag",
171
+ identity: snapshot.tag,
172
+ }));
173
+ let afterSequence = 0;
174
+ let finished = false;
175
+ while (!finished) {
176
+ const page = await this.readSourcePage(source, {
177
+ serviceId: snapshot.serviceId,
178
+ tag: snapshot.tag,
179
+ upperBoundSequence: snapshot.upperBoundSequence,
180
+ afterSequence,
181
+ limit: PAGE_SIZE,
182
+ });
183
+ this.assertPage(snapshot, afterSequence, page);
184
+ for (const obligation of page.rows) {
185
+ // `obligation_sequence` is local to one Tag DO, but it is a
186
+ // durable contiguous range inside that partition. A gap or a
187
+ // duplicate is therefore not an innocuous pagination detail: it
188
+ // means the source universe cannot be proved complete.
189
+ if (obligation.obligationSequence !== afterSequence + 1 || obligation.obligationSequence > snapshot.upperBoundSequence) {
190
+ throw new Error(`source_page_sequence_outside_snapshot:${snapshot.tag}`);
191
+ }
192
+ afterSequence = obligation.obligationSequence;
193
+ scanned += 1;
194
+ let joined;
195
+ try {
196
+ joined = this.options.globalReceiptMatcher === undefined
197
+ ? await this.globalReceiptMatches(snapshot.serviceId, snapshot.tag, obligation)
198
+ : await this.options.globalReceiptMatcher(snapshot.serviceId, snapshot.tag, obligation);
199
+ }
200
+ catch (error) {
201
+ await this.appendFinding(snapshot.serviceId, {
202
+ identity: `GLOBAL_RECEIPT_UNAVAILABLE|${snapshot.serviceId}|${snapshot.tag}|${obligation.obligationSequence}|${obligation.eventId}|${obligation.eventDigest}`,
203
+ type: "GLOBAL_ARRAY_RECEIPT_UNAVAILABLE",
204
+ partitionTag: snapshot.tag,
205
+ obligationSequence: obligation.obligationSequence,
206
+ eventId: obligation.eventId,
207
+ eventDigest: obligation.eventDigest,
208
+ }, nowMs).catch(() => undefined);
209
+ throw error;
210
+ }
211
+ if (!joined) {
212
+ await this.appendObligationFinding(snapshot.serviceId, snapshot.tag, obligation, nowMs);
213
+ findings += 1;
214
+ }
215
+ }
216
+ if (!page.hasMore) {
217
+ finished = true;
218
+ }
219
+ else if (page.rows.length === 0) {
220
+ throw new Error(`source_page_truncated:${snapshot.tag}`);
221
+ }
222
+ }
223
+ // All local sequences are retained under G43, so a non-empty bound
224
+ // that yielded no row is source-unreadable, never a healthy scan.
225
+ if (snapshot.upperBoundSequence > 0 && afterSequence === 0) {
226
+ throw new Error(`source_partition_missing_rows:${snapshot.tag}`);
227
+ }
228
+ if (afterSequence !== snapshot.upperBoundSequence) {
229
+ throw new Error(`source_partition_range_incomplete:${snapshot.tag}`);
230
+ }
231
+ }
232
+ catch (error) {
233
+ if (errorText(error).startsWith("source_")) {
234
+ await this.appendFinding(snapshot.serviceId, {
235
+ identity: sourceUnavailableIdentity(snapshot.serviceId, snapshot.tag),
236
+ type: "GLOBAL_ARRAY_SOURCE_PARTITION_UNAVAILABLE",
237
+ partitionTag: snapshot.tag,
238
+ obligationSequence: null,
239
+ eventId: null,
240
+ eventDigest: null,
241
+ }, nowMs).catch(() => undefined);
242
+ }
243
+ throw error;
244
+ }
245
+ }
246
+ // The proof domain is the start-of-pass snapshot. A partition added
247
+ // after that snapshot is intentionally left for the next pass; it must
248
+ // not invalidate the proof already established for the partitions that
249
+ // were actually walked. A removed start partition is different because
250
+ // the pass can no longer be said to have covered its proof domain.
251
+ const endSnapshots = await this.snapshotPartitions(serviceId);
252
+ this.assertStartPartitionsRetained(snapshots, endSnapshots);
253
+ if (findings > 0) {
254
+ // Keep the historical G44 diagnostic for an unresolved obligation
255
+ // discovered while the universe was changing. It remains fail-closed
256
+ // and does not grant a frontier; the all-joined case above is the
257
+ // bounded AC2 path that may settle the start-of-pass snapshot.
258
+ this.assertSnapshotUniverseUnchanged(snapshots, endSnapshots);
259
+ }
260
+ const cursor = {
261
+ schema: "sdt-g58-settled-frontier/v1",
262
+ snapshots,
263
+ frontierSuid: await this.settledFrontierAtSnapshot(serviceId, snapshots),
264
+ };
265
+ if (findings > 0) {
266
+ // An unresolved source obligation is deliberately not a completed
267
+ // frontier. Retaining a vector cursor here would let a later reader
268
+ // mistake partial coverage for a successfully closed range.
269
+ await this.writeHealth(serviceId, "BLOCK", null, null, "source present/global receipt absent", nowMs);
270
+ return { kind: "BLOCK", partitions: snapshots, findingCount: findings };
271
+ }
272
+ await this.writeHealth(serviceId, "HEALTHY", JSON.stringify(cursor), nowMs, null, nowMs);
273
+ return { kind: "FULL", partitions: snapshots, scannedObligations: scanned };
274
+ }
275
+ catch (error) {
276
+ const message = errorText(error);
277
+ const health = message.startsWith("source_") ? "UNKNOWN" : "FAILED";
278
+ if (health === "FAILED") {
279
+ await this.appendFinding(serviceId, {
280
+ identity: scannerFailureIdentity(serviceId),
281
+ type: "GLOBAL_ARRAY_SCANNER_FAILURE",
282
+ partitionTag: null,
283
+ obligationSequence: null,
284
+ eventId: null,
285
+ eventDigest: null,
286
+ }, nowMs).catch(() => undefined);
287
+ }
288
+ await this.writeHealth(serviceId, health, null, null, message, nowMs).catch(() => undefined);
289
+ return health === "UNKNOWN" ? { kind: "UNKNOWN", reason: message } : { kind: "FAILED", error: message };
290
+ }
291
+ }
292
+ async readSourcePage(source, input) {
293
+ const url = new URL("https://g44-source.internal/__internal/g44/source-obligations");
294
+ url.searchParams.set("__serviceId", input.serviceId);
295
+ url.searchParams.set("__tag", input.tag);
296
+ const response = await source.fetch(new Request(url, {
297
+ method: "POST",
298
+ headers: { "content-type": "application/json", "x-sdt-g44-source-scan": "1" },
299
+ body: JSON.stringify(input),
300
+ }));
301
+ if (!response.ok) {
302
+ throw new Error(`source_partition_unreadable:${response.status}`);
303
+ }
304
+ return sourcePageFrom(await response.json());
305
+ }
306
+ /**
307
+ * Read the detector health authority without using findings, delivery, or
308
+ * Queue state as a proxy. A never-run or expired successful scan is never
309
+ * silently healthy.
310
+ */
311
+ async readHealth(serviceId, nowMs, staleAfterMs = G44_HEALTH_STALE_AFTER_MS) {
312
+ if (!Number.isSafeInteger(nowMs) || !Number.isSafeInteger(staleAfterMs) || staleAfterMs < 1) {
313
+ throw new Error("g44_health_read_invalid_time");
314
+ }
315
+ const row = await this.database.prepare(`SELECT service_id, scanner_version, status, cursor_json, last_full_scan_at, last_error, updated_at
316
+ FROM serialized_dcb_completeness_scanner_health
317
+ WHERE service_id = ?`).bind(serviceId).first();
318
+ if (row === null || row === undefined) {
319
+ return {
320
+ serviceId,
321
+ scannerVersion: this.scannerVersion,
322
+ status: "UNKNOWN",
323
+ cursorJson: null,
324
+ lastSettledFrontierSuid: null,
325
+ lastFullScanAt: null,
326
+ lastError: "scanner_has_never_run",
327
+ updatedAt: nowMs,
328
+ };
329
+ }
330
+ const status = asString(row.status, "scanner_health.status");
331
+ const lastFullScanAt = row.last_full_scan_at === null ? null : asNumber(row.last_full_scan_at, "scanner_health.last_full_scan_at");
332
+ if (status === "HEALTHY" && (lastFullScanAt === null || nowMs - lastFullScanAt > staleAfterMs)) {
333
+ await this.writeHealth(serviceId, "STALE", null, lastFullScanAt, "scanner_full_scan_stale", nowMs);
334
+ return {
335
+ serviceId,
336
+ scannerVersion: asString(row.scanner_version, "scanner_health.scanner_version"),
337
+ status: "STALE",
338
+ cursorJson: row.cursor_json === null ? null : asString(row.cursor_json, "scanner_health.cursor_json"),
339
+ lastSettledFrontierSuid: settledFrontierFromCursor(row.cursor_json === null ? null : asString(row.cursor_json, "scanner_health.cursor_json")),
340
+ lastFullScanAt,
341
+ lastError: "scanner_full_scan_stale",
342
+ updatedAt: nowMs,
343
+ };
344
+ }
345
+ if (!["HEALTHY", "UNKNOWN", "FAILED", "STALE", "BLOCK", "UNSETTLED"].includes(status)) {
346
+ throw new Error("scanner_health_status_invalid");
347
+ }
348
+ return {
349
+ serviceId: asString(row.service_id, "scanner_health.service_id"),
350
+ scannerVersion: asString(row.scanner_version, "scanner_health.scanner_version"),
351
+ status,
352
+ cursorJson: row.cursor_json === null ? null : asString(row.cursor_json, "scanner_health.cursor_json"),
353
+ lastSettledFrontierSuid: settledFrontierFromCursor(row.cursor_json === null ? null : asString(row.cursor_json, "scanner_health.cursor_json")),
354
+ lastFullScanAt,
355
+ lastError: row.last_error === null ? null : asString(row.last_error, "scanner_health.last_error"),
356
+ updatedAt: asNumber(row.updated_at, "scanner_health.updated_at"),
357
+ };
358
+ }
359
+ /**
360
+ * The one interim coverage gate. It deliberately has no configuration
361
+ * matrix and does not turn a missing finding into a synthetic healthy read.
362
+ */
363
+ async coverage(serviceId, nowMs) {
364
+ const health = await this.readHealth(serviceId, nowMs);
365
+ if (health.status === "HEALTHY") {
366
+ return {
367
+ kind: "SETTLED",
368
+ health,
369
+ frontierSuid: health.lastSettledFrontierSuid,
370
+ reason: null,
371
+ partitionTag: null,
372
+ observedAt: health.updatedAt,
373
+ };
374
+ }
375
+ return {
376
+ kind: "BLOCK/UNSETTLED",
377
+ health,
378
+ frontierSuid: health.lastSettledFrontierSuid,
379
+ reason: health.lastError ?? `scanner_${health.status.toLowerCase()}`,
380
+ partitionTag: await this.latestFindingPartitionTag(serviceId),
381
+ observedAt: health.updatedAt,
382
+ };
383
+ }
384
+ /**
385
+ * The downstream view gate must consume the same proof domain that the
386
+ * reconciler persisted. A service-level HEALTHY bit is insufficient: a
387
+ * delivery from a partition added after that scan must remain blocked until
388
+ * a later successful cursor includes its exact local obligation sequence.
389
+ */
390
+ async coverageForObligation(serviceId, tag, obligationSequence, nowMs) {
391
+ if (serviceId.length === 0 || tag.length === 0 ||
392
+ !Number.isSafeInteger(obligationSequence) || obligationSequence < 1) {
393
+ throw new Error("g44_coverage_obligation_invalid_identity");
394
+ }
395
+ const decision = await this.coverage(serviceId, nowMs);
396
+ if (decision.kind !== "SETTLED" || cursorIncludesObligation(decision.health.cursorJson, serviceId, tag, obligationSequence)) {
397
+ return decision;
398
+ }
399
+ return {
400
+ kind: "BLOCK/UNSETTLED",
401
+ health: decision.health,
402
+ frontierSuid: decision.frontierSuid,
403
+ reason: `obligation_not_in_settled_cursor:${tag}:${obligationSequence}`,
404
+ partitionTag: tag,
405
+ observedAt: decision.observedAt,
406
+ };
407
+ }
408
+ /**
409
+ * DeliveryCore calls this after a detector exception and before it returns
410
+ * without applying views. It shares the scanner's single health/finding
411
+ * authority rather than creating a second incident workflow.
412
+ */
413
+ async recordDetectorFailure(serviceId, error, nowMs) {
414
+ const message = `detector_failure:${errorText(error).slice(0, 256)}`;
415
+ await this.appendFinding(serviceId, {
416
+ identity: `GLOBAL_DETECTOR_FAILURE|${serviceId}`,
417
+ type: "GLOBAL_ARRAY_DETECTOR_FAILURE",
418
+ partitionTag: null,
419
+ obligationSequence: null,
420
+ eventId: null,
421
+ eventDigest: null,
422
+ }, nowMs);
423
+ await this.writeHealth(serviceId, "FAILED", null, null, message, nowMs);
424
+ }
425
+ /** D1 receipt join is independent from every projection and transport. */
426
+ async globalReceiptMatches(serviceId, tag, obligation) {
427
+ const membership = obligation.localCommittedMembership[0];
428
+ if (membership === undefined || obligation.localCommittedMembership.length !== 1 ||
429
+ membership.serviceId !== serviceId || membership.eventId !== obligation.eventId || membership.tag !== tag)
430
+ return false;
431
+ const row = await this.database.prepare(`SELECT 1 AS joined
432
+ FROM serialized_dcb_global_receipts AS receipt
433
+ JOIN serialized_dcb_global_memberships AS membership
434
+ ON membership.service_id = receipt.service_id
435
+ AND membership.event_id = receipt.event_id
436
+ AND membership.partition_tag = receipt.membership_tag
437
+ AND membership.event_digest = receipt.event_digest
438
+ JOIN dcb_events AS event
439
+ ON event."ServiceId" = receipt.service_id
440
+ AND event."Id" = receipt.event_id
441
+ AND event."EventDigest" = receipt.event_digest
442
+ WHERE receipt.service_id = ? AND receipt.partition_tag = ?
443
+ AND receipt.obligation_sequence = ? AND receipt.event_id = ?
444
+ AND receipt.event_digest = ? AND receipt.membership_tag = ?`).bind(serviceId, tag, obligation.obligationSequence, obligation.eventId, obligation.eventDigest, tag).first();
445
+ return row !== null && row !== undefined && row.joined === 1;
446
+ }
447
+ async snapshotPartitions(serviceId) {
448
+ const result = await this.database.prepare(`SELECT service_id, partition_tag, last_obligation_sequence
449
+ FROM serialized_dcb_source_partitions
450
+ WHERE service_id = ?
451
+ ORDER BY partition_tag COLLATE BINARY ASC`).bind(serviceId).all();
452
+ const snapshots = result.results.map((row) => ({
453
+ serviceId: asString(row.service_id, "source_partition.service_id"),
454
+ tag: asString(row.partition_tag, "source_partition.partition_tag"),
455
+ upperBoundSequence: asNumber(row.last_obligation_sequence, "source_partition.last_obligation_sequence"),
456
+ }));
457
+ const identities = new Set();
458
+ for (const snapshot of snapshots) {
459
+ const identity = `${snapshot.serviceId}\u0000${snapshot.tag}`;
460
+ if (identities.has(identity))
461
+ throw new Error(`source_partition_duplicate:${snapshot.tag}`);
462
+ identities.add(identity);
463
+ }
464
+ return snapshots;
465
+ }
466
+ assertSnapshotUniverseUnchanged(start, end) {
467
+ const startSet = new Set(start.map((entry) => `${entry.serviceId}\u0000${entry.tag}`));
468
+ const endSet = new Set(end.map((entry) => `${entry.serviceId}\u0000${entry.tag}`));
469
+ if (startSet.size !== endSet.size || [...startSet].some((entry) => !endSet.has(entry))) {
470
+ throw new Error("source_partition_set_changed_during_scan");
471
+ }
472
+ }
473
+ assertStartPartitionsRetained(start, end) {
474
+ const endSet = new Set(end.map((entry) => `${entry.serviceId}\u0000${entry.tag}`));
475
+ if (start.some((entry) => !endSet.has(`${entry.serviceId}\u0000${entry.tag}`))) {
476
+ throw new Error("source_partition_set_changed_during_scan");
477
+ }
478
+ }
479
+ assertPage(snapshot, afterSequence, page) {
480
+ if (page.serviceId !== snapshot.serviceId || page.tag !== snapshot.tag ||
481
+ page.upperBoundSequence !== snapshot.upperBoundSequence ||
482
+ page.afterSequence !== afterSequence || page.observedMaxSequence < snapshot.upperBoundSequence) {
483
+ throw new Error(`source_page_snapshot_mismatch:${snapshot.tag}`);
484
+ }
485
+ }
486
+ /**
487
+ * The frontier is derived only from receipts inside the immutable source
488
+ * snapshot that just passed every G44 join. It is not the mutable global
489
+ * table head: a delivery arriving after the snapshot is deliberately left
490
+ * for a later FULL scan.
491
+ */
492
+ async settledFrontierAtSnapshot(serviceId, snapshots) {
493
+ let frontier = null;
494
+ for (const snapshot of snapshots) {
495
+ const row = await this.database.prepare(`SELECT MAX(event."SortableUniqueId" COLLATE BINARY) AS frontier_suid
496
+ FROM serialized_dcb_global_receipts receipt
497
+ JOIN dcb_events event
498
+ ON event."ServiceId" = receipt.service_id
499
+ AND event."Id" = receipt.event_id
500
+ WHERE receipt.service_id = ?
501
+ AND receipt.partition_tag = ?
502
+ AND receipt.obligation_sequence <= ?`).bind(serviceId, snapshot.tag, snapshot.upperBoundSequence).first();
503
+ const candidate = row?.frontier_suid;
504
+ if (typeof candidate === "string" && candidate.length > 0 && (frontier === null || candidate > frontier)) {
505
+ frontier = candidate;
506
+ }
507
+ }
508
+ return frontier;
509
+ }
510
+ async latestFindingPartitionTag(serviceId) {
511
+ const row = await this.database.prepare(`SELECT partition_tag
512
+ FROM serialized_dcb_completeness_findings
513
+ WHERE service_id = ? AND partition_tag IS NOT NULL
514
+ ORDER BY last_observed_at DESC, partition_tag COLLATE BINARY ASC
515
+ LIMIT 1`).bind(serviceId).first();
516
+ return row === null || row === undefined || row.partition_tag === null
517
+ ? null
518
+ : asString(row.partition_tag, "scanner_health.partition_tag");
519
+ }
520
+ async appendObligationFinding(serviceId, tag, obligation, nowMs) {
521
+ const incidentType = obligation.status === "poison"
522
+ ? "GLOBAL_ARRAY_POISON_OBLIGATION"
523
+ : "GLOBAL_ARRAY_RECEIPT_ABSENT";
524
+ await this.appendFinding(serviceId, {
525
+ identity: findingIdentity(serviceId, tag, obligation),
526
+ type: incidentType,
527
+ partitionTag: tag,
528
+ obligationSequence: obligation.obligationSequence,
529
+ eventId: obligation.eventId,
530
+ eventDigest: obligation.eventDigest,
531
+ }, nowMs);
532
+ }
533
+ async appendFinding(serviceId, finding, nowMs) {
534
+ await this.database.prepare(`INSERT INTO serialized_dcb_completeness_findings
535
+ (service_id, incident_identity, incident_type, partition_tag,
536
+ obligation_sequence, event_id, event_digest, state,
537
+ first_observed_at, last_observed_at)
538
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'OPEN', ?, ?)
539
+ ON CONFLICT (service_id, incident_identity) DO UPDATE SET
540
+ last_observed_at = excluded.last_observed_at`).bind(serviceId, finding.identity, finding.type, finding.partitionTag, finding.obligationSequence, finding.eventId, finding.eventDigest, nowMs, nowMs).run();
541
+ }
542
+ async writeHealth(serviceId, status, cursor, lastFullScanAt, error, nowMs) {
543
+ await this.database.prepare(`INSERT INTO serialized_dcb_completeness_scanner_health
544
+ (service_id, scanner_version, status, cursor_json, last_full_scan_at, last_error, updated_at)
545
+ VALUES (?, ?, ?, ?, ?, ?, ?)
546
+ ON CONFLICT (service_id) DO UPDATE SET
547
+ scanner_version = excluded.scanner_version,
548
+ status = excluded.status,
549
+ cursor_json = COALESCE(excluded.cursor_json, serialized_dcb_completeness_scanner_health.cursor_json),
550
+ last_full_scan_at = COALESCE(excluded.last_full_scan_at, serialized_dcb_completeness_scanner_health.last_full_scan_at),
551
+ last_error = excluded.last_error,
552
+ updated_at = excluded.updated_at`).bind(serviceId, this.scannerVersion, status, cursor, lastFullScanAt, error, nowMs).run();
553
+ }
554
+ }
555
+ /** The acknowledgement payload carries facts from the same source envelope. */
556
+ export function globalReceiptAcknowledgement(message, receivedAt) {
557
+ return {
558
+ deliveries: [{
559
+ attemptId: message.attemptId,
560
+ eventId: message.eventId,
561
+ suid: message.suid,
562
+ payload: message.payload,
563
+ eventType: message.eventType,
564
+ provenance: message.provenance,
565
+ timestamp: message.timestamp,
566
+ allocatorLineageId: message.allocatorLineageId,
567
+ enqueuedAt: message.enqueuedAt,
568
+ completeness: message.completeness,
569
+ }],
570
+ nowMs: receivedAt,
571
+ };
572
+ }
@@ -0,0 +1,85 @@
1
+ import type { OutboxCommittedMembership } from "../downstream/types";
2
+ /** Fixed scanner identity; changing its semantics requires a new source build. */
3
+ export declare const G44_SCANNER_VERSION = "sdt-g44-global-completeness/v1";
4
+ export type GlobalCompletenessHealth = "HEALTHY" | "UNKNOWN" | "FAILED" | "STALE" | "BLOCK" | "UNSETTLED";
5
+ /** A health read is never inferred from the absence of a finding. */
6
+ export interface GlobalCompletenessHealthRecord {
7
+ readonly serviceId: string;
8
+ readonly scannerVersion: string;
9
+ readonly status: GlobalCompletenessHealth;
10
+ readonly cursorJson: string | null;
11
+ /**
12
+ * The high-water SUID from the most recent FULL source snapshot. It is
13
+ * deliberately retained while a later scan is BLOCK/UNSETTLED so a safe
14
+ * consumer can continue only through already-proven contiguous work.
15
+ */
16
+ readonly lastSettledFrontierSuid: string | null;
17
+ readonly lastFullScanAt: number | null;
18
+ readonly lastError: string | null;
19
+ readonly updatedAt: number;
20
+ }
21
+ /** A deliberately conservative liveness bound for an otherwise idle scanner. */
22
+ export declare const G44_HEALTH_STALE_AFTER_MS: number;
23
+ /** A vector cursor component: local obligation sequence is only tag-local. */
24
+ export interface SourcePartitionSnapshot {
25
+ readonly serviceId: string;
26
+ readonly tag: string;
27
+ readonly upperBoundSequence: number;
28
+ }
29
+ /** Source-local fact returned by the Tag DO, independent of transport state. */
30
+ export interface SourceObligationFact {
31
+ readonly obligationSequence: number;
32
+ readonly eventId: string;
33
+ readonly eventDigest: string;
34
+ readonly canonicalBytesBase64: string;
35
+ readonly declaredTagSet: readonly string[];
36
+ readonly localCommittedMembership: readonly OutboxCommittedMembership[];
37
+ readonly status: "pending" | "acknowledged" | "poison";
38
+ }
39
+ /** A bounded page within one fixed partition snapshot component. */
40
+ export interface SourceObligationPage {
41
+ readonly serviceId: string;
42
+ readonly tag: string;
43
+ readonly upperBoundSequence: number;
44
+ readonly observedMaxSequence: number;
45
+ readonly afterSequence: number;
46
+ readonly rows: readonly SourceObligationFact[];
47
+ readonly hasMore: boolean;
48
+ }
49
+ export type GlobalCompletenessScanResult = Readonly<{
50
+ kind: "FULL";
51
+ partitions: readonly SourcePartitionSnapshot[];
52
+ scannedObligations: number;
53
+ }> | Readonly<{
54
+ kind: "BLOCK";
55
+ partitions: readonly SourcePartitionSnapshot[];
56
+ findingCount: number;
57
+ }> | Readonly<{
58
+ kind: "UNKNOWN";
59
+ reason: string;
60
+ }> | Readonly<{
61
+ kind: "FAILED";
62
+ error: string;
63
+ }>;
64
+ /** The sole temporary operational disposition; policy decisions remain open. */
65
+ export declare const GLOBAL_COMPLETENESS_INTERIM_DISPOSITION: "BLOCK/UNSETTLED";
66
+ /**
67
+ * This is an internal coverage decision, not a new public response schema.
68
+ * Callers may advance a materialized/live view only after a concrete FULL
69
+ * scanner health record; every other health state stays BLOCK/UNSETTLED.
70
+ */
71
+ export type GlobalCompletenessCoverage = Readonly<{
72
+ kind: "SETTLED";
73
+ health: GlobalCompletenessHealthRecord;
74
+ frontierSuid: string | null;
75
+ reason: null;
76
+ partitionTag: null;
77
+ observedAt: number;
78
+ }> | Readonly<{
79
+ kind: typeof GLOBAL_COMPLETENESS_INTERIM_DISPOSITION;
80
+ health: GlobalCompletenessHealthRecord;
81
+ frontierSuid: string | null;
82
+ reason: string;
83
+ partitionTag: string | null;
84
+ observedAt: number;
85
+ }>;
@@ -0,0 +1,6 @@
1
+ /** Fixed scanner identity; changing its semantics requires a new source build. */
2
+ export const G44_SCANNER_VERSION = "sdt-g44-global-completeness/v1";
3
+ /** A deliberately conservative liveness bound for an otherwise idle scanner. */
4
+ export const G44_HEALTH_STALE_AFTER_MS = 5 * 60_000;
5
+ /** The sole temporary operational disposition; policy decisions remain open. */
6
+ export const GLOBAL_COMPLETENESS_INTERIM_DISPOSITION = "BLOCK/UNSETTLED";