@adcp/sdk 14.0.0-beta.26 → 14.0.0-beta.28

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 (66) hide show
  1. package/dist/lib/core/AgentClient.d.mts +5 -1
  2. package/dist/lib/core/AgentClient.d.ts +5 -1
  3. package/dist/lib/core/SingleAgentClient.js +2 -0
  4. package/dist/lib/core/SingleAgentClient.mjs +2 -0
  5. package/dist/lib/index.d.mts +1 -0
  6. package/dist/lib/index.d.ts +1 -0
  7. package/dist/lib/index.js +2 -0
  8. package/dist/lib/index.mjs +1 -0
  9. package/dist/lib/reporting/evidence.d.mts +4 -0
  10. package/dist/lib/reporting/evidence.d.ts +4 -0
  11. package/dist/lib/reporting/evidence.js +136 -0
  12. package/dist/lib/reporting/evidence.mjs +109 -0
  13. package/dist/lib/reporting/index.d.mts +4 -0
  14. package/dist/lib/reporting/index.d.ts +4 -0
  15. package/dist/lib/reporting/index.js +45 -0
  16. package/dist/lib/reporting/index.mjs +24 -0
  17. package/dist/lib/reporting/inspection.d.mts +88 -0
  18. package/dist/lib/reporting/inspection.d.ts +88 -0
  19. package/dist/lib/reporting/inspection.js +1125 -0
  20. package/dist/lib/reporting/inspection.mjs +1094 -0
  21. package/dist/lib/reporting/reconciliation.d.mts +199 -0
  22. package/dist/lib/reporting/reconciliation.d.ts +199 -0
  23. package/dist/lib/reporting/reconciliation.js +870 -0
  24. package/dist/lib/reporting/reconciliation.mjs +844 -0
  25. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  26. package/dist/lib/server/decisioning/runtime/protocol-for-tool.js +1 -0
  27. package/dist/lib/server/decisioning/runtime/protocol-for-tool.mjs +1 -0
  28. package/dist/lib/server/wire-spec-fields.generated.d.mts +11 -1
  29. package/dist/lib/server/wire-spec-fields.generated.d.ts +11 -1
  30. package/dist/lib/server/wire-spec-fields.generated.js +10 -0
  31. package/dist/lib/server/wire-spec-fields.generated.mjs +10 -0
  32. package/dist/lib/testing/storyboard/runner.js +13 -2
  33. package/dist/lib/testing/storyboard/runner.mjs +13 -2
  34. package/dist/lib/testing/storyboard/types.d.mts +8 -0
  35. package/dist/lib/testing/storyboard/types.d.ts +8 -0
  36. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  37. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  38. package/dist/lib/types/create-media-buy.d.ts +21 -16
  39. package/dist/lib/types/get-media-buys.d.ts +21 -16
  40. package/dist/lib/types/index.d.mts +1 -0
  41. package/dist/lib/types/index.d.ts +1 -0
  42. package/dist/lib/types/list-accounts.d.ts +21 -16
  43. package/dist/lib/types/list-creatives.d.ts +21 -16
  44. package/dist/lib/types/schemas.generated.d.ts +688 -952
  45. package/dist/lib/types/schemas.generated.js +542 -515
  46. package/dist/lib/types/schemas.generated.mjs +542 -515
  47. package/dist/lib/types/sync-accounts.d.ts +21 -16
  48. package/dist/lib/types/sync-creatives.d.ts +21 -16
  49. package/dist/lib/types/tools.generated.d.mts +2 -6
  50. package/dist/lib/types/tools.generated.d.ts +2 -6
  51. package/dist/lib/utils/reporting-status-response.d.mts +2 -0
  52. package/dist/lib/utils/reporting-status-response.d.ts +2 -0
  53. package/dist/lib/utils/reporting-status-response.js +338 -0
  54. package/dist/lib/utils/reporting-status-response.mjs +319 -0
  55. package/dist/lib/utils/response-schemas.js +6 -1
  56. package/dist/lib/utils/response-schemas.mjs +6 -1
  57. package/dist/lib/validation/sync-creatives.d.mts +42 -108
  58. package/dist/lib/validation/sync-creatives.d.ts +42 -108
  59. package/dist/lib/version.d.mts +3 -3
  60. package/dist/lib/version.d.ts +3 -3
  61. package/dist/lib/version.js +3 -3
  62. package/dist/lib/version.mjs +3 -3
  63. package/docs/TYPE-SUMMARY.md +2 -2
  64. package/docs/guides/REPORTING-RECONCILIATION.md +107 -0
  65. package/docs/llms.txt +2 -2
  66. package/package.json +1 -1
@@ -0,0 +1,844 @@
1
+ import { createHash } from "crypto";
2
+ import { generateIdempotencyKey } from "../utils/idempotency.mjs";
3
+ import { isReportingControlTotals, isReportingReceiptEvidence, isReportingVerificationEvidence } from "./evidence.mjs";
4
+ import {
5
+ createReportingManifestInspector,
6
+ ReportingInspectionError
7
+ } from "./inspection.mjs";
8
+ class ReportingReconciliationError extends Error {
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.code = code;
12
+ this.name = "ReportingReconciliationError";
13
+ }
14
+ code;
15
+ }
16
+ async function callBeforeDeadline(operation, deadline, code, message) {
17
+ const remainingMs = deadline - Date.now();
18
+ if (remainingMs <= 0) throw new ReportingReconciliationError(code, message);
19
+ const controller = new AbortController();
20
+ let timer;
21
+ const timeout = new Promise((_resolve, reject) => {
22
+ timer = setTimeout(() => {
23
+ controller.abort();
24
+ reject(new ReportingReconciliationError(code, message));
25
+ }, remainingMs);
26
+ });
27
+ try {
28
+ return await Promise.race([operation(controller.signal), timeout]);
29
+ } finally {
30
+ if (timer) clearTimeout(timer);
31
+ }
32
+ }
33
+ function canonical(value) {
34
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
35
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
36
+ const entries = Object.entries(value).filter(([, child]) => child !== void 0).sort(([left], [right]) => left.localeCompare(right));
37
+ return `{${entries.map(([key, child]) => `${JSON.stringify(key)}:${canonical(child)}`).join(",")}}`;
38
+ }
39
+ function same(left, right) {
40
+ return canonical(left) === canonical(right);
41
+ }
42
+ function sameSha256(left, right) {
43
+ return Boolean(
44
+ left && right && /^[a-fA-F0-9]{64}$/.test(left) && /^[a-fA-F0-9]{64}$/.test(right) && left.toLowerCase() === right.toLowerCase()
45
+ );
46
+ }
47
+ function sameCanonicalDigest(left, right) {
48
+ if (!left || !right) return false;
49
+ const leftWithUri = left;
50
+ const rightWithUri = right;
51
+ return left.algorithm === right.algorithm && sameSha256(left.value, right.value) && left.canonicalization_id === right.canonicalization_id && sameSha256(left.canonicalization_sha256, right.canonicalization_sha256) && leftWithUri.canonicalization_uri === rightWithUri.canonicalization_uri;
52
+ }
53
+ function uniqueStrings(value) {
54
+ return Array.isArray(value) && value.every((item) => typeof item === "string" && item.length > 0) && new Set(value).size === value.length;
55
+ }
56
+ function sameStringSet(left, right) {
57
+ return same([...left].sort(), [...right].sort());
58
+ }
59
+ function isReportingCoverageEvidence(value) {
60
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
61
+ const coverage = value;
62
+ const allowedKeys = /* @__PURE__ */ new Set([
63
+ "status",
64
+ "evaluated_at",
65
+ "media_buy_ids",
66
+ "fully_covered_media_buy_ids",
67
+ "partially_covered_media_buy_ids",
68
+ "unsupported_media_buy_ids",
69
+ "unknown_media_buy_ids",
70
+ "package_ids",
71
+ "covered_package_ids",
72
+ "unsupported_package_ids",
73
+ "unknown_package_ids",
74
+ "limitations"
75
+ ]);
76
+ if (Object.keys(coverage).some((key) => !allowedKeys.has(key)) || !["full", "partial", "none", "unknown"].includes(coverage.status) || typeof coverage.evaluated_at !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(coverage.evaluated_at) || !Number.isFinite(Date.parse(coverage.evaluated_at))) {
77
+ return false;
78
+ }
79
+ const arrays = [
80
+ coverage.media_buy_ids,
81
+ coverage.fully_covered_media_buy_ids,
82
+ coverage.partially_covered_media_buy_ids,
83
+ coverage.unsupported_media_buy_ids,
84
+ coverage.unknown_media_buy_ids,
85
+ coverage.package_ids,
86
+ coverage.covered_package_ids,
87
+ coverage.unsupported_package_ids,
88
+ coverage.unknown_package_ids
89
+ ];
90
+ if (!arrays.every(uniqueStrings) || !Array.isArray(coverage.limitations)) return false;
91
+ const buyParts = [
92
+ ...coverage.fully_covered_media_buy_ids,
93
+ ...coverage.partially_covered_media_buy_ids,
94
+ ...coverage.unsupported_media_buy_ids,
95
+ ...coverage.unknown_media_buy_ids
96
+ ];
97
+ const packageParts = [
98
+ ...coverage.covered_package_ids,
99
+ ...coverage.unsupported_package_ids,
100
+ ...coverage.unknown_package_ids
101
+ ];
102
+ if (new Set(buyParts).size !== buyParts.length || new Set(packageParts).size !== packageParts.length || !sameStringSet(buyParts, coverage.media_buy_ids) || !sameStringSet(packageParts, coverage.package_ids)) {
103
+ return false;
104
+ }
105
+ const limitationReasons = /* @__PURE__ */ new Set([
106
+ "offering_unsupported",
107
+ "account_entitlement_unavailable",
108
+ "credential_scope_insufficient",
109
+ "provider_limitation",
110
+ "capability_unknown"
111
+ ]);
112
+ for (const limitation of coverage.limitations) {
113
+ if (!limitation || typeof limitation !== "object" || Array.isArray(limitation) || !Object.keys(limitation).every((key) => ["reason", "media_buy_id", "package_ids"].includes(key)) || !limitationReasons.has(limitation.reason) || typeof limitation.media_buy_id !== "string" || !coverage.media_buy_ids.includes(limitation.media_buy_id) || limitation.package_ids !== void 0 && (!uniqueStrings(limitation.package_ids) || limitation.package_ids.length === 0 || !limitation.package_ids.every((id) => coverage.package_ids.includes(id)))) {
114
+ return false;
115
+ }
116
+ }
117
+ const full = coverage.partially_covered_media_buy_ids.length === 0 && coverage.unsupported_media_buy_ids.length === 0 && coverage.unknown_media_buy_ids.length === 0 && coverage.unsupported_package_ids.length === 0 && coverage.unknown_package_ids.length === 0 && sameStringSet(coverage.fully_covered_media_buy_ids, coverage.media_buy_ids) && sameStringSet(coverage.covered_package_ids, coverage.package_ids);
118
+ if (coverage.status === "full") return full;
119
+ const nonempty = coverage.media_buy_ids.length > 0 || coverage.package_ids.length > 0;
120
+ const hasCovered = coverage.fully_covered_media_buy_ids.length > 0 || coverage.covered_package_ids.length > 0;
121
+ const hasUncovered = coverage.partially_covered_media_buy_ids.length > 0 || coverage.unsupported_media_buy_ids.length > 0 || coverage.unknown_media_buy_ids.length > 0 || coverage.unsupported_package_ids.length > 0 || coverage.unknown_package_ids.length > 0;
122
+ if (coverage.status === "partial") return hasCovered && hasUncovered;
123
+ if (coverage.status === "none")
124
+ return nonempty && coverage.fully_covered_media_buy_ids.length === 0 && coverage.partially_covered_media_buy_ids.length === 0 && coverage.covered_package_ids.length === 0 && coverage.unknown_media_buy_ids.length === 0 && coverage.unknown_package_ids.length === 0 && (coverage.unsupported_media_buy_ids.length > 0 || coverage.unsupported_package_ids.length > 0);
125
+ return coverage.status === "unknown" && nonempty && !hasCovered && coverage.partially_covered_media_buy_ids.length === 0 && (coverage.unknown_media_buy_ids.length > 0 || coverage.unknown_package_ids.length > 0);
126
+ }
127
+ function coverageMatchesExpected(coverage, expected) {
128
+ const comparable = {
129
+ status: coverage.status,
130
+ media_buy_ids: coverage.media_buy_ids,
131
+ fully_covered_media_buy_ids: coverage.fully_covered_media_buy_ids,
132
+ partially_covered_media_buy_ids: coverage.partially_covered_media_buy_ids,
133
+ unsupported_media_buy_ids: coverage.unsupported_media_buy_ids,
134
+ unknown_media_buy_ids: coverage.unknown_media_buy_ids,
135
+ package_ids: coverage.package_ids,
136
+ covered_package_ids: coverage.covered_package_ids,
137
+ unsupported_package_ids: coverage.unsupported_package_ids,
138
+ unknown_package_ids: coverage.unknown_package_ids
139
+ };
140
+ return same(comparable, expected);
141
+ }
142
+ function scopeMatchesRequest(scope, request) {
143
+ if (request.period && (scope.period_start !== request.period.start || scope.period_end !== request.period.end)) {
144
+ return false;
145
+ }
146
+ if (request.media_buy_ids) {
147
+ if (scope.all_accessible_media_buys || !sameStringSet(scope.media_buy_ids ?? [], request.media_buy_ids))
148
+ return false;
149
+ } else if (!scope.all_accessible_media_buys) {
150
+ return false;
151
+ }
152
+ if (request.delivery_config_ids) {
153
+ const resolved = [...new Set(scope.delivery_config_generations.map((item) => item.delivery_config_id))];
154
+ if (!sameStringSet(resolved, request.delivery_config_ids)) return false;
155
+ }
156
+ if (request.feed_purposes && !sameStringSet(scope.feed_purposes, request.feed_purposes)) return false;
157
+ if (request.finality && !sameStringSet(scope.finality, request.finality)) return false;
158
+ return true;
159
+ }
160
+ function normalizedTotals(totals) {
161
+ return [...totals].sort((left, right) => left.name.localeCompare(right.name));
162
+ }
163
+ function receiptMatches(receipt, revision, materialization) {
164
+ if (!isReportingReceiptEvidence(receipt) || receipt.status !== "accepted" || !materialization.verification || !isReportingVerificationEvidence(materialization.verification) || !isReportingControlTotals(revision.control_totals)) {
165
+ return false;
166
+ }
167
+ if (receipt.reporting_obligation_id !== materialization.reporting_obligation_id) return false;
168
+ if (receipt.reporting_revision_id !== revision.reporting_revision_id) return false;
169
+ if (receipt.reporting_materialization_id !== materialization.reporting_materialization_id) return false;
170
+ if (receipt.verification_profile !== materialization.verification.verification_profile) return false;
171
+ if (receipt.observed_row_count !== revision.row_count) return false;
172
+ if (!same(normalizedTotals(receipt.observed_control_totals), normalizedTotals(revision.control_totals))) return false;
173
+ if (receipt.verification_profile === "canonical_digest") {
174
+ return Boolean(
175
+ revision.canonical_content_digest && receipt.observed_canonical_content_digest && sameCanonicalDigest(receipt.observed_canonical_content_digest, revision.canonical_content_digest)
176
+ );
177
+ }
178
+ if (receipt.verification_profile === "manifest_checksums") {
179
+ return Boolean(
180
+ materialization.resource?.manifest_sha256 && sameSha256(receipt.observed_manifest_sha256, materialization.resource.manifest_sha256)
181
+ );
182
+ }
183
+ return Boolean(
184
+ materialization.resource?.native_version_ref && receipt.observed_native_version_ref === materialization.resource.native_version_ref
185
+ );
186
+ }
187
+ function addImmutable(map, id, value, kind) {
188
+ const previous = map.get(id);
189
+ if (previous && !same(previous, value)) {
190
+ throw new ReportingReconciliationError(
191
+ "IMMUTABLE_RECORD_CHANGED",
192
+ `${kind} ${id} changed within one ledger snapshot`
193
+ );
194
+ }
195
+ map.set(id, value);
196
+ }
197
+ async function loadReportingLedger(client, request, maxSnapshotRestarts = 2, limits = {}) {
198
+ if (!Number.isSafeInteger(maxSnapshotRestarts) || maxSnapshotRestarts < 0 || maxSnapshotRestarts > 10) {
199
+ throw new ReportingReconciliationError(
200
+ "INVALID_LEDGER_LIMITS",
201
+ "maxSnapshotRestarts must be an integer from 0 through 10"
202
+ );
203
+ }
204
+ const requestedAccountId = request.account.account_id;
205
+ const maxPages = limits.maxPages ?? 1e3;
206
+ const maxRecords = limits.maxRecords ?? 1e5;
207
+ const maxLoadMs = limits.maxLoadMs ?? 6e4;
208
+ if (!Number.isSafeInteger(maxPages) || maxPages < 1 || maxPages > 1e4) {
209
+ throw new ReportingReconciliationError("INVALID_LEDGER_LIMITS", "maxPages must be an integer from 1 through 10000");
210
+ }
211
+ if (!Number.isSafeInteger(maxRecords) || maxRecords < 1 || maxRecords > 1e6) {
212
+ throw new ReportingReconciliationError(
213
+ "INVALID_LEDGER_LIMITS",
214
+ "maxRecords must be an integer from 1 through 1000000"
215
+ );
216
+ }
217
+ if (!Number.isSafeInteger(maxLoadMs) || maxLoadMs < 1 || maxLoadMs > 36e5) {
218
+ throw new ReportingReconciliationError(
219
+ "INVALID_LEDGER_LIMITS",
220
+ "maxLoadMs must be an integer from 1 through 3600000"
221
+ );
222
+ }
223
+ const deadline = Date.now() + maxLoadMs;
224
+ for (let restart = 0; restart <= maxSnapshotRestarts; restart += 1) {
225
+ try {
226
+ const obligations = /* @__PURE__ */ new Map();
227
+ const revisions = /* @__PURE__ */ new Map();
228
+ const materializations = /* @__PURE__ */ new Map();
229
+ const receipts = /* @__PURE__ */ new Map();
230
+ const seenCursors = /* @__PURE__ */ new Set();
231
+ let cursor;
232
+ let snapshotId;
233
+ let ledgerAsOf;
234
+ let accountId;
235
+ let scope;
236
+ let totalCount;
237
+ let pageCount = 0;
238
+ do {
239
+ pageCount += 1;
240
+ if (pageCount > maxPages || Date.now() > deadline) {
241
+ throw new ReportingReconciliationError("LEDGER_LIMIT_EXCEEDED", "reporting ledger exceeded load limits");
242
+ }
243
+ const response = await callBeforeDeadline(
244
+ (signal) => client.getReportingStatus(
245
+ {
246
+ ...request,
247
+ view: "periods",
248
+ ...cursor ? { pagination: { cursor } } : {}
249
+ },
250
+ { signal }
251
+ ),
252
+ deadline,
253
+ "LEDGER_LIMIT_EXCEEDED",
254
+ "get_reporting_status exceeded the reporting ledger load deadline"
255
+ );
256
+ if (response.status !== "completed" || response.view !== "periods") {
257
+ throw new ReportingReconciliationError(
258
+ "STATUS_READ_FAILED",
259
+ "get_reporting_status did not return a completed periods view"
260
+ );
261
+ }
262
+ if (!response.ledger_snapshot_id || !response.ledger_as_of || !response.account_id || !response.scope || !response.pagination) {
263
+ throw new ReportingReconciliationError(
264
+ "INCOMPLETE_LEDGER_PAGE",
265
+ "get_reporting_status omitted required ledger metadata"
266
+ );
267
+ }
268
+ if (typeof requestedAccountId === "string" && response.account_id !== requestedAccountId) {
269
+ throw new ReportingReconciliationError(
270
+ "ACCOUNT_SCOPE_MISMATCH",
271
+ "get_reporting_status returned a ledger for a different requested account"
272
+ );
273
+ }
274
+ if (!scopeMatchesRequest(response.scope, request)) {
275
+ throw new ReportingReconciliationError(
276
+ "REQUEST_SCOPE_MISMATCH",
277
+ "get_reporting_status returned a denominator that does not match the requested scope"
278
+ );
279
+ }
280
+ if (typeof response.pagination.total_count !== "number" || !Number.isSafeInteger(response.pagination.total_count) || response.pagination.total_count < 0) {
281
+ throw new ReportingReconciliationError(
282
+ "INCOMPLETE_LEDGER_PAGE",
283
+ "get_reporting_status returned an invalid total_count"
284
+ );
285
+ }
286
+ if (snapshotId && snapshotId !== response.ledger_snapshot_id) {
287
+ throw new ReportingReconciliationError("SNAPSHOT_CHANGED", "ledger snapshot changed during pagination");
288
+ }
289
+ if (ledgerAsOf && ledgerAsOf !== response.ledger_as_of) {
290
+ throw new ReportingReconciliationError(
291
+ "SNAPSHOT_CHANGED",
292
+ "ledger observation boundary changed during pagination"
293
+ );
294
+ }
295
+ if (accountId && accountId !== response.account_id) {
296
+ throw new ReportingReconciliationError("SNAPSHOT_CHANGED", "account changed during pagination");
297
+ }
298
+ if (scope && !same(scope, response.scope)) {
299
+ throw new ReportingReconciliationError("SNAPSHOT_CHANGED", "reporting denominator changed during pagination");
300
+ }
301
+ if (totalCount !== void 0 && response.pagination.total_count !== totalCount) {
302
+ throw new ReportingReconciliationError("SNAPSHOT_CHANGED", "ledger total changed during pagination");
303
+ }
304
+ snapshotId = response.ledger_snapshot_id;
305
+ ledgerAsOf = response.ledger_as_of;
306
+ accountId = response.account_id;
307
+ scope = response.scope;
308
+ totalCount = response.pagination.total_count;
309
+ if (totalCount > maxRecords) {
310
+ throw new ReportingReconciliationError("LEDGER_LIMIT_EXCEEDED", "reporting ledger exceeds record limit");
311
+ }
312
+ for (const item of response.periods ?? [])
313
+ addImmutable(obligations, item.reporting_obligation_id, item, "obligation");
314
+ for (const item of response.revisions ?? [])
315
+ addImmutable(revisions, item.reporting_revision_id, item, "revision");
316
+ for (const item of response.materializations ?? [])
317
+ addImmutable(materializations, item.reporting_materialization_id, item, "materialization");
318
+ for (const item of response.receipts ?? []) addImmutable(receipts, item.reporting_receipt_id, item, "receipt");
319
+ if (obligations.size + revisions.size + materializations.size + receipts.size > maxRecords) {
320
+ throw new ReportingReconciliationError("LEDGER_LIMIT_EXCEEDED", "reporting ledger exceeds record limit");
321
+ }
322
+ if (response.pagination.has_more) {
323
+ if (!response.pagination.cursor || seenCursors.has(response.pagination.cursor)) {
324
+ throw new ReportingReconciliationError("CURSOR_LOOP", "ledger pagination did not advance");
325
+ }
326
+ seenCursors.add(response.pagination.cursor);
327
+ cursor = response.pagination.cursor;
328
+ } else {
329
+ cursor = void 0;
330
+ }
331
+ } while (cursor);
332
+ const observedCount = obligations.size + revisions.size + materializations.size + receipts.size;
333
+ if (totalCount !== void 0 && totalCount !== observedCount) {
334
+ throw new ReportingReconciliationError(
335
+ "LEDGER_COUNT_MISMATCH",
336
+ `ledger declared ${totalCount} records but returned ${observedCount}`
337
+ );
338
+ }
339
+ if (!snapshotId || !ledgerAsOf || !accountId || !scope) {
340
+ throw new ReportingReconciliationError("EMPTY_LEDGER_RESPONSE", "get_reporting_status returned no ledger page");
341
+ }
342
+ assertReportingLedgerGraph(accountId, obligations, revisions, materializations, receipts);
343
+ return {
344
+ ledgerSnapshotId: snapshotId,
345
+ ledgerAsOf,
346
+ accountId,
347
+ scope,
348
+ obligations: [...obligations.values()],
349
+ revisions: [...revisions.values()],
350
+ materializations: [...materializations.values()],
351
+ receipts: [...receipts.values()]
352
+ };
353
+ } catch (error) {
354
+ if (!(error instanceof ReportingReconciliationError) || error.code !== "SNAPSHOT_CHANGED" || restart === maxSnapshotRestarts) {
355
+ throw error;
356
+ }
357
+ }
358
+ }
359
+ throw new ReportingReconciliationError("SNAPSHOT_CHANGED", "ledger never stabilized");
360
+ }
361
+ function assertReportingLedgerGraph(accountId, obligations, revisions, materializations, receipts) {
362
+ const fail = () => {
363
+ throw new ReportingReconciliationError(
364
+ "LEDGER_GRAPH_INTEGRITY_FAILED",
365
+ "reporting ledger contains an out-of-scope or unjoined record"
366
+ );
367
+ };
368
+ for (const obligation of obligations.values()) {
369
+ if (obligation.account_id !== accountId) fail();
370
+ }
371
+ const referencedRevisions = /* @__PURE__ */ new Set();
372
+ for (const materialization of materializations.values()) {
373
+ const obligation = obligations.get(materialization.reporting_obligation_id);
374
+ const revision = revisions.get(materialization.reporting_revision_id);
375
+ if (!obligation || !revision || revision.account_id !== accountId || materialization.delivery_config_id !== obligation.delivery_config_id || materialization.delivery_config_version !== obligation.delivery_config_version || materialization.destination_ref !== obligation.destination_ref || materialization.feed_purpose !== obligation.feed_purpose || (materialization.status === "available" || materialization.status === "delivered") && !isReportingVerificationEvidence(materialization.verification)) {
376
+ fail();
377
+ }
378
+ referencedRevisions.add(materialization.reporting_revision_id);
379
+ }
380
+ for (const revision of revisions.values()) {
381
+ if (revision.account_id !== accountId || !referencedRevisions.has(revision.reporting_revision_id) || !isReportingControlTotals(revision.control_totals)) {
382
+ fail();
383
+ }
384
+ }
385
+ for (const receipt of receipts.values()) {
386
+ const obligation = obligations.get(receipt.reporting_obligation_id);
387
+ const revision = revisions.get(receipt.reporting_revision_id);
388
+ const materialization = materializations.get(receipt.reporting_materialization_id);
389
+ if (!isReportingReceiptEvidence(receipt) || !obligation || !revision || !materialization || materialization.reporting_obligation_id !== obligation.reporting_obligation_id || materialization.reporting_revision_id !== revision.reporting_revision_id) {
390
+ fail();
391
+ }
392
+ }
393
+ }
394
+ function assertDirectReportingLedgerGraph(ledger) {
395
+ const obligations = new Map(ledger.obligations.map((item) => [item.reporting_obligation_id, item]));
396
+ const revisions = new Map(ledger.revisions.map((item) => [item.reporting_revision_id, item]));
397
+ const materializations = new Map(ledger.materializations.map((item) => [item.reporting_materialization_id, item]));
398
+ const receipts = new Map(ledger.receipts.map((item) => [item.reporting_receipt_id, item]));
399
+ if (obligations.size !== ledger.obligations.length || revisions.size !== ledger.revisions.length || materializations.size !== ledger.materializations.length || receipts.size !== ledger.receipts.length) {
400
+ throw new ReportingReconciliationError(
401
+ "LEDGER_GRAPH_INTEGRITY_FAILED",
402
+ "reporting ledger contains duplicate record identifiers"
403
+ );
404
+ }
405
+ assertReportingLedgerGraph(ledger.accountId, obligations, revisions, materializations, receipts);
406
+ }
407
+ function selectCurrent(obligation, ledger, expected) {
408
+ const reasons = [];
409
+ const attempts = ledger.materializations.filter(
410
+ (item) => item.reporting_obligation_id === obligation.reporting_obligation_id
411
+ );
412
+ const revisionIds = new Set(attempts.map((item) => item.reporting_revision_id));
413
+ const candidates = ledger.revisions.filter((item) => revisionIds.has(item.reporting_revision_id));
414
+ const receipts = ledger.receipts.filter((item) => item.reporting_obligation_id === obligation.reporting_obligation_id);
415
+ const successfulAttempts = attempts.filter((item) => item.status === "available" || item.status === "delivered");
416
+ const acceptedReceipts = receipts.filter((item) => item.status === "accepted");
417
+ if (obligation.account_id !== ledger.accountId) reasons.push("OBLIGATION_ACCOUNT_MISMATCH");
418
+ if (candidates.length !== obligation.revision_count || attempts.length !== obligation.materialization_count || successfulAttempts.length !== obligation.successful_materialization_count || receipts.length !== obligation.receipt_count || acceptedReceipts.length !== obligation.accepted_receipt_count) {
419
+ reasons.push("ASSOCIATED_HISTORY_INCOMPLETE");
420
+ }
421
+ const superseded = new Set(
422
+ candidates.map((item) => item.supersedes_reporting_revision_id).filter((id) => Boolean(id))
423
+ );
424
+ const candidateIds = new Set(candidates.map((item) => item.reporting_revision_id));
425
+ if (candidates.some(
426
+ (item) => item.supersedes_reporting_revision_id && !candidateIds.has(item.supersedes_reporting_revision_id)
427
+ )) {
428
+ reasons.push("REVISION_PREDECESSOR_MISSING");
429
+ }
430
+ if (candidates.some(
431
+ (item) => item.account_id !== obligation.account_id || item.report_definition_id !== obligation.report_definition_id || item.reporting_profile !== obligation.reporting_profile || !Array.isArray(item.media_buy_ids) || !Array.isArray(obligation.media_buy_ids) || !same([...item.media_buy_ids].sort(), [...obligation.media_buy_ids].sort()) || !same(item.period, obligation.period)
432
+ )) {
433
+ reasons.push("REVISION_CHAIN_SCOPE_MISMATCH");
434
+ }
435
+ const current = candidates.filter((item) => !superseded.has(item.reporting_revision_id));
436
+ if (current.length !== 1) {
437
+ reasons.push(current.length === 0 ? "MISSING_CURRENT_REVISION" : "AMBIGUOUS_REVISION_CHAIN");
438
+ return { reasons };
439
+ }
440
+ const revision = current[0];
441
+ const revisionControlTotalsValid = isReportingControlTotals(revision.control_totals);
442
+ if (!revisionControlTotalsValid) reasons.push("REVISION_CONTROL_TOTALS_INVALID");
443
+ if (!revision.report_definition_uri || !revision.report_definition_sha256) {
444
+ reasons.push("REPORT_DEFINITION_NOT_PINNED");
445
+ }
446
+ if (revision.account_id !== obligation.account_id || revision.report_definition_id !== obligation.report_definition_id || revision.reporting_profile !== obligation.reporting_profile || !Array.isArray(revision.media_buy_ids) || !Array.isArray(obligation.media_buy_ids) || !same([...revision.media_buy_ids].sort(), [...obligation.media_buy_ids].sort()) || !same(revision.period, obligation.period)) {
447
+ reasons.push("REVISION_SCOPE_MISMATCH");
448
+ }
449
+ if (obligation.scope_resolved_at !== obligation.period.end) reasons.push("SCOPE_CUTOFF_MISMATCH");
450
+ if (!isReportingCoverageEvidence(obligation.coverage) || obligation.coverage.evaluated_at !== obligation.scope_resolved_at || !sameStringSet(obligation.coverage.media_buy_ids, obligation.media_buy_ids ?? []) || !isReportingCoverageEvidence(revision.coverage) || !same(revision.coverage, obligation.coverage)) {
451
+ reasons.push("COVERAGE_SCOPE_MISMATCH");
452
+ }
453
+ if (!expected) {
454
+ reasons.push("EXPECTED_CONTRACT_MISSING");
455
+ } else {
456
+ const revisionDigest = revision.canonical_content_digest;
457
+ if (revision.report_definition_uri !== expected.reportDefinitionUri || !sameSha256(revision.report_definition_sha256, expected.reportDefinitionSha256) || revision.schema_version !== expected.schemaVersion || revision.schema_uri !== expected.schemaUri || !sameSha256(revision.schema_sha256, expected.schemaSha256) || revision.schema_dialect !== expected.schemaDialect || revision.schema_ref_policy !== expected.schemaRefPolicy) {
458
+ reasons.push("EXPECTED_CONTRACT_MISMATCH");
459
+ }
460
+ if (!isReportingCoverageEvidence(obligation.coverage) || !coverageMatchesExpected(obligation.coverage, expected.coverage)) {
461
+ reasons.push("EXPECTED_COVERAGE_MISMATCH");
462
+ }
463
+ if (expected.coverageRequirement === "full" && obligation.coverage?.status !== "full") {
464
+ reasons.push("COVERAGE_REQUIREMENT_NOT_MET");
465
+ }
466
+ if (expected.verificationProfile === "canonical_digest" && (!revisionDigest || revisionDigest.canonicalization_id !== expected.canonicalization.id || revisionDigest.canonicalization_uri !== expected.canonicalization.uri || !sameSha256(revisionDigest.canonicalization_sha256, expected.canonicalization.sha256))) {
467
+ reasons.push("EXPECTED_CANONICALIZATION_MISMATCH");
468
+ }
469
+ }
470
+ const finalizedAt = revision.finalized_at ? Date.parse(revision.finalized_at) : Number.NaN;
471
+ const periodEnd = Date.parse(revision.period.end);
472
+ const createdAt = Date.parse(revision.created_at);
473
+ if (obligation.required_finality === "official" && revision.finality !== "official" || revision.finality === "official" && (!revision.finality_basis || !revision.finality_policy_id || !revision.finalized_at || !Number.isFinite(finalizedAt) || finalizedAt < periodEnd || finalizedAt > createdAt)) {
474
+ reasons.push("FINALITY_NOT_MET");
475
+ }
476
+ if (revision.finality === "official" && (!expected?.officialFinality || revision.finality_policy_id !== expected.officialFinality.policyId || revision.finality_basis !== expected.officialFinality.basis)) {
477
+ reasons.push("EXPECTED_FINALITY_POLICY_MISMATCH");
478
+ }
479
+ const successful = successfulAttempts.filter(
480
+ (item) => item.reporting_revision_id === revision.reporting_revision_id && (item.status === "available" || item.status === "delivered")
481
+ ).sort((left, right) => right.attempt - left.attempt);
482
+ const materialization = successful[0];
483
+ if (!materialization?.verification || !materialization.resource) {
484
+ reasons.push("MISSING_VERIFIED_MATERIALIZATION");
485
+ return { revision, reasons };
486
+ }
487
+ const verificationEvidenceValid = isReportingVerificationEvidence(materialization.verification);
488
+ if (!verificationEvidenceValid) {
489
+ reasons.push("PRODUCER_VERIFICATION_EVIDENCE_INVALID");
490
+ }
491
+ const methodEvidenceValid = Boolean(materialization.ready_at) && (materialization.method === "file_transfer" && materialization.resource.kind === "manifest" && materialization.resource.manifest_version === "1.0" && Boolean(materialization.resource.manifest_sha256) && Boolean(materialization.verification.physical_checksums?.length) || materialization.method === "dataset_share" && materialization.resource.kind === "dataset" && materialization.verification.verification_path === "representative_consumer" || materialization.method === "warehouse_materialization" && materialization.resource.kind === "warehouse_relation" && materialization.verification.verification_path === "destination");
492
+ if (!methodEvidenceValid) reasons.push("MATERIALIZATION_METHOD_EVIDENCE_MISMATCH");
493
+ if (materialization.resource.immutability === "native_version" && !materialization.resource.native_version_ref) {
494
+ reasons.push("MATERIALIZATION_RESOURCE_EVIDENCE_MISMATCH");
495
+ }
496
+ if (expected && materialization.verification.verification_profile !== expected.verificationProfile) {
497
+ reasons.push("EXPECTED_VERIFICATION_PROFILE_MISMATCH");
498
+ }
499
+ if (expected && materialization.method !== expected.deliveryMethod) {
500
+ reasons.push("EXPECTED_DELIVERY_METHOD_MISMATCH");
501
+ }
502
+ if (materialization.method === "file_transfer" && !materialization.verification.physical_checksums?.length) {
503
+ reasons.push("PRODUCER_PHYSICAL_CHECKSUMS_MISSING");
504
+ }
505
+ if (materialization.delivery_config_id !== obligation.delivery_config_id || materialization.delivery_config_version !== obligation.delivery_config_version || materialization.destination_ref !== obligation.destination_ref || materialization.feed_purpose !== obligation.feed_purpose) {
506
+ reasons.push("MATERIALIZATION_SCOPE_MISMATCH");
507
+ }
508
+ if (verificationEvidenceValid && revisionControlTotalsValid && materialization.verification.row_count !== revision.row_count || verificationEvidenceValid && revisionControlTotalsValid && !same(normalizedTotals(materialization.verification.control_totals), normalizedTotals(revision.control_totals))) {
509
+ reasons.push("PRODUCER_CONTROL_TOTAL_MISMATCH");
510
+ }
511
+ if (materialization.verification.verification_profile === "canonical_digest" && (!revision.canonical_content_digest || !sameCanonicalDigest(materialization.verification.canonical_content_digest, revision.canonical_content_digest))) {
512
+ reasons.push("PRODUCER_DIGEST_MISMATCH");
513
+ }
514
+ if (obligation.feed_purpose === "billing" && materialization.verification.verification_profile !== "canonical_digest") {
515
+ reasons.push("BILLING_VERIFICATION_PROFILE_MISMATCH");
516
+ }
517
+ if (materialization.verification.verification_profile === "native_commit") {
518
+ const evidence = materialization.verification.native_commit_evidence;
519
+ if (!evidence || !materialization.resource.native_version_ref || evidence.native_version_ref !== materialization.resource.native_version_ref || evidence.observed_through !== materialization.verification.verification_path) {
520
+ reasons.push("PRODUCER_NATIVE_EVIDENCE_MISMATCH");
521
+ }
522
+ }
523
+ if (materialization.verification.verification_profile === "manifest_checksums") {
524
+ if (materialization.resource.kind !== "manifest" || materialization.resource.manifest_version !== "1.0" || !materialization.resource.manifest_sha256 || !materialization.verification.physical_checksums?.length) {
525
+ reasons.push("PRODUCER_MANIFEST_EVIDENCE_MISSING");
526
+ }
527
+ }
528
+ return { revision, materialization, reasons };
529
+ }
530
+ function expectedPeriodMatches(expected, obligation, ledger) {
531
+ if (obligation.delivery_config_id !== expected.deliveryConfigId || obligation.delivery_config_version !== expected.deliveryConfigVersion || obligation.report_definition_id !== expected.reportDefinitionId || obligation.feed_purpose !== expected.feedPurpose || obligation.reporting_profile !== expected.reportingProfile || !Array.isArray(obligation.media_buy_ids) || !same([...obligation.media_buy_ids].sort(), [...expected.mediaBuyIds].sort()) || obligation.destination_ref !== expected.destinationRef || obligation.required_finality !== expected.requiredFinality || obligation.reconciliation_mode !== expected.reconciliationMode || expected.coverageRequirement === "full" && obligation.coverage?.status !== "full" || obligation.period.start !== expected.periodStart || obligation.period.end !== expected.periodEnd || !isReportingCoverageEvidence(obligation.coverage) || !coverageMatchesExpected(obligation.coverage, expected.coverage)) {
532
+ return false;
533
+ }
534
+ const attempts = ledger.materializations.filter(
535
+ (materialization) => materialization.reporting_obligation_id === obligation.reporting_obligation_id
536
+ );
537
+ return attempts.length === 0 || attempts.every((materialization) => materialization.method === expected.deliveryMethod) && attempts.filter((materialization) => materialization.status === "available" || materialization.status === "delivered").every((materialization) => materialization.verification?.verification_profile === expected.verificationProfile);
538
+ }
539
+ function expectedIdentityKey(value) {
540
+ if ("deliveryConfigId" in value) {
541
+ return canonical([
542
+ value.deliveryConfigId,
543
+ value.deliveryConfigVersion,
544
+ value.reportDefinitionId,
545
+ value.feedPurpose,
546
+ value.reportingProfile,
547
+ value.destinationRef,
548
+ value.periodStart,
549
+ value.periodEnd
550
+ ]);
551
+ }
552
+ return canonical([
553
+ value.delivery_config_id,
554
+ value.delivery_config_version,
555
+ value.report_definition_id,
556
+ value.feed_purpose,
557
+ value.reporting_profile,
558
+ value.destination_ref,
559
+ value.period.start,
560
+ value.period.end
561
+ ]);
562
+ }
563
+ function buildExpectedIdentityIndex(obligations, expectedPeriods) {
564
+ const expectedByIdentity = /* @__PURE__ */ new Map();
565
+ const obligationCounts = /* @__PURE__ */ new Map();
566
+ for (const expected of expectedPeriods) {
567
+ const key = expectedIdentityKey(expected);
568
+ expectedByIdentity.set(key, [...expectedByIdentity.get(key) ?? [], expected]);
569
+ }
570
+ for (const obligation of obligations) {
571
+ const key = expectedIdentityKey(obligation);
572
+ obligationCounts.set(key, (obligationCounts.get(key) ?? 0) + 1);
573
+ }
574
+ return { expectedByIdentity, obligationCounts };
575
+ }
576
+ function evaluateReportingLedger(ledger, expectedPeriods, now = /* @__PURE__ */ new Date()) {
577
+ assertDirectReportingLedgerGraph(ledger);
578
+ const obligationResults = [];
579
+ const uniqueRevisions = /* @__PURE__ */ new Map();
580
+ const { expectedByIdentity, obligationCounts } = buildExpectedIdentityIndex(
581
+ ledger.obligations,
582
+ expectedPeriods ?? []
583
+ );
584
+ for (const obligation of ledger.obligations) {
585
+ const identity = expectedIdentityKey(obligation);
586
+ const matchingExpected = expectedByIdentity.get(identity) ?? [];
587
+ const bijective = matchingExpected.length === 1 && obligationCounts.get(identity) === 1;
588
+ const expected = bijective ? matchingExpected[0] : void 0;
589
+ const selected = selectCurrent(obligation, ledger, expected);
590
+ const reasons = [...selected.reasons];
591
+ if (matchingExpected.length > 0 && !bijective) reasons.push("EXPECTED_PERIOD_NOT_BIJECTIVE");
592
+ if (obligation.health !== "complete") reasons.push(`OBLIGATION_${obligation.health.toUpperCase()}`);
593
+ if (selected.materialization?.resource && new Date(selected.materialization.resource.expires_at) <= now)
594
+ reasons.push("RESOURCE_EXPIRED");
595
+ if (!obligation.resource_retained_until || selected.materialization?.resource && Date.parse(selected.materialization.resource.expires_at) < Date.parse(obligation.resource_retained_until)) {
596
+ reasons.push("RESOURCE_RETENTION_MISMATCH");
597
+ }
598
+ if (selected.revision) uniqueRevisions.set(selected.revision.reporting_revision_id, selected.revision);
599
+ if (obligation.reconciliation_mode === "consumer_receipt" && selected.revision && selected.materialization) {
600
+ const accepted = ledger.receipts.some(
601
+ (receipt) => receiptMatches(receipt, selected.revision, selected.materialization)
602
+ );
603
+ if (!accepted) reasons.push("MISSING_MATCHING_CONSUMER_RECEIPT");
604
+ }
605
+ obligationResults.push({
606
+ reportingObligationId: obligation.reporting_obligation_id,
607
+ definitive: reasons.length === 0,
608
+ reportingRevisionId: selected.revision?.reporting_revision_id,
609
+ reportingMaterializationId: selected.materialization?.reporting_materialization_id,
610
+ reasons
611
+ });
612
+ }
613
+ const missingExpectedPeriods = (expectedPeriods ?? []).filter(
614
+ (expected) => !ledger.obligations.some((obligation) => expectedPeriodMatches(expected, obligation, ledger))
615
+ );
616
+ const scopeDefinitive = ledger.scope.scope_closed && ledger.scope.coverage_complete;
617
+ return {
618
+ definitive: expectedPeriods !== void 0 && scopeDefinitive && missingExpectedPeriods.length === 0 && obligationResults.every((item) => item.definitive),
619
+ ledger,
620
+ obligations: obligationResults,
621
+ missingExpectedPeriods,
622
+ totalsByRevision: [...uniqueRevisions.values()].map((item) => ({
623
+ reportingRevisionId: item.reporting_revision_id,
624
+ rowCount: item.row_count,
625
+ controlTotals: item.control_totals,
626
+ coverageStatus: item.coverage?.status ?? "unknown",
627
+ coveredPackageIds: item.coverage?.covered_package_ids ?? [],
628
+ packageIds: item.coverage?.package_ids ?? []
629
+ }))
630
+ };
631
+ }
632
+ function buildReportingReceipt(context, observation, reportingReceiptId = `reporting-receipt:${generateIdempotencyKey()}`, observedAt = (/* @__PURE__ */ new Date()).toISOString()) {
633
+ const { obligation, revision, materialization } = context;
634
+ if (!materialization.verification || !materialization.resource) {
635
+ throw new ReportingReconciliationError("MATERIALIZATION_NOT_READY", "cannot receipt an unverified materialization");
636
+ }
637
+ const rejectionCodes = [];
638
+ if (observation.rowCount !== revision.row_count) rejectionCodes.push("ROW_COUNT_MISMATCH");
639
+ if (!same(normalizedTotals(observation.controlTotals), normalizedTotals(revision.control_totals)))
640
+ rejectionCodes.push("CONTROL_TOTAL_MISMATCH");
641
+ const profile = materialization.verification.verification_profile;
642
+ if (profile === "canonical_digest" && (!revision.canonical_content_digest || !sameCanonicalDigest(observation.canonicalContentDigest, revision.canonical_content_digest))) {
643
+ rejectionCodes.push("CANONICAL_DIGEST_MISMATCH");
644
+ }
645
+ if (profile === "manifest_checksums" && !sameSha256(observation.manifestSha256, materialization.resource.manifest_sha256)) {
646
+ rejectionCodes.push("MANIFEST_DIGEST_MISMATCH");
647
+ }
648
+ if (profile === "native_commit" && observation.nativeVersionRef !== materialization.resource.native_version_ref) {
649
+ rejectionCodes.push("NATIVE_VERSION_MISMATCH");
650
+ }
651
+ const [firstRejectionCode, ...remainingRejectionCodes] = rejectionCodes;
652
+ return {
653
+ reporting_receipt_id: reportingReceiptId,
654
+ reporting_obligation_id: obligation.reporting_obligation_id,
655
+ reporting_revision_id: revision.reporting_revision_id,
656
+ reporting_materialization_id: materialization.reporting_materialization_id,
657
+ status: rejectionCodes.length === 0 ? "accepted" : "rejected",
658
+ verification_profile: profile,
659
+ observed_row_count: observation.rowCount,
660
+ observed_control_totals: observation.controlTotals,
661
+ ...observation.canonicalContentDigest ? { observed_canonical_content_digest: observation.canonicalContentDigest } : {},
662
+ ...observation.manifestSha256 ? { observed_manifest_sha256: observation.manifestSha256 } : {},
663
+ ...observation.nativeVersionRef ? { observed_native_version_ref: observation.nativeVersionRef } : {},
664
+ ...observation.consumerCommitRef ? { consumer_commit_ref: observation.consumerCommitRef } : {},
665
+ ...firstRejectionCode !== void 0 ? { rejection_codes: [firstRejectionCode, ...remainingRejectionCodes] } : {},
666
+ observed_at: observedAt
667
+ };
668
+ }
669
+ async function inspectWithRetry(inspect, context, maxAttempts, retryBaseDelayMs) {
670
+ let lastError;
671
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
672
+ try {
673
+ return await inspect(context);
674
+ } catch (error) {
675
+ lastError = error;
676
+ if (error instanceof ReportingInspectionError && !error.retryable) throw error;
677
+ if (attempt < maxAttempts && retryBaseDelayMs > 0) {
678
+ const delayMs = Math.min(retryBaseDelayMs * 2 ** (attempt - 1), 5e3);
679
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
680
+ }
681
+ }
682
+ }
683
+ if (lastError instanceof ReportingInspectionError) throw lastError;
684
+ throw new ReportingReconciliationError(
685
+ "INSPECTION_FAILED",
686
+ `materialization inspection failed after ${maxAttempts} attempts`
687
+ );
688
+ }
689
+ function buildCheckpointKey(consumerScope, accountId, context) {
690
+ return {
691
+ consumerScope,
692
+ accountId,
693
+ reportingObligationId: context.obligation.reporting_obligation_id,
694
+ reportingRevisionId: context.revision.reporting_revision_id,
695
+ reportingMaterializationId: context.materialization.reporting_materialization_id,
696
+ destinationRef: context.materialization.destination_ref
697
+ };
698
+ }
699
+ function checkpointMatchesContext(checkpoint, context) {
700
+ const { receipt } = checkpoint;
701
+ return Boolean(
702
+ checkpoint.receiptSyncIdempotencyKey && checkpoint.contextFingerprint === checkpointContextFingerprint(context) && context.materialization.verification && receipt.reporting_obligation_id === context.obligation.reporting_obligation_id && receipt.reporting_revision_id === context.revision.reporting_revision_id && receipt.reporting_materialization_id === context.materialization.reporting_materialization_id && receipt.verification_profile === context.materialization.verification.verification_profile
703
+ );
704
+ }
705
+ function checkpointContextFingerprint(context) {
706
+ return createHash("sha256").update(canonical(context)).digest("hex");
707
+ }
708
+ async function reconcileReporting(options) {
709
+ if (!options.inspect && (!options.resourceReader || !options.manifestInspectorOptions?.referenceAllowedOrigins?.length)) {
710
+ throw new ReportingReconciliationError(
711
+ "INSPECTOR_CONFIGURATION_REQUIRED",
712
+ "Built-in inspection requires resourceReader and consumer-approved reference origins"
713
+ );
714
+ }
715
+ if (options.checkpointStore && !options.checkpointScope) {
716
+ throw new ReportingReconciliationError(
717
+ "CHECKPOINT_SCOPE_REQUIRED",
718
+ "checkpointStore requires a stable seller and authenticated-principal scope"
719
+ );
720
+ }
721
+ const maxInspectionAttempts = options.maxInspectionAttempts ?? 3;
722
+ const inspectionRetryBaseDelayMs = options.inspectionRetryBaseDelayMs ?? 100;
723
+ if (!Number.isSafeInteger(maxInspectionAttempts) || maxInspectionAttempts < 1 || maxInspectionAttempts > 10) {
724
+ throw new ReportingReconciliationError(
725
+ "INVALID_INSPECTION_RETRY_POLICY",
726
+ "maxInspectionAttempts must be an integer from 1 through 10"
727
+ );
728
+ }
729
+ if (!Number.isSafeInteger(inspectionRetryBaseDelayMs) || inspectionRetryBaseDelayMs < 0 || inspectionRetryBaseDelayMs > 6e4) {
730
+ throw new ReportingReconciliationError(
731
+ "INVALID_INSPECTION_RETRY_POLICY",
732
+ "inspectionRetryBaseDelayMs must be an integer from 0 through 60000"
733
+ );
734
+ }
735
+ let ledger = await loadReportingLedger(
736
+ options.client,
737
+ options.request,
738
+ options.maxSnapshotRestarts,
739
+ options.ledgerLimits
740
+ );
741
+ const newReceipts = [];
742
+ const pendingSubmissions = [];
743
+ const inspect = options.inspect ?? (options.resourceReader ? createReportingManifestInspector({
744
+ ...options.manifestInspectorOptions,
745
+ reader: options.resourceReader,
746
+ credentialProvider: options.credentialProvider
747
+ }) : void 0);
748
+ const { expectedByIdentity, obligationCounts } = buildExpectedIdentityIndex(
749
+ ledger.obligations,
750
+ options.expectedPeriods
751
+ );
752
+ for (const obligation of ledger.obligations) {
753
+ if (obligation.reconciliation_mode !== "consumer_receipt") continue;
754
+ const identity = expectedIdentityKey(obligation);
755
+ const matches = expectedByIdentity.get(identity) ?? [];
756
+ if (matches.length !== 1 || obligationCounts.get(identity) !== 1) continue;
757
+ const expected = matches[0];
758
+ const selected = selectCurrent(obligation, ledger, expected);
759
+ if (!selected.revision || !selected.materialization || selected.reasons.length) continue;
760
+ if (ledger.receipts.some((receipt) => receiptMatches(receipt, selected.revision, selected.materialization)))
761
+ continue;
762
+ if (!inspect) {
763
+ throw new ReportingReconciliationError(
764
+ "INSPECTOR_REQUIRED",
765
+ "Provide inspect or resourceReader for consumer-receipt reconciliation"
766
+ );
767
+ }
768
+ const context = {
769
+ obligation,
770
+ revision: selected.revision,
771
+ materialization: selected.materialization,
772
+ expected
773
+ };
774
+ const checkpointKey = buildCheckpointKey(options.checkpointScope ?? "ephemeral", ledger.accountId, context);
775
+ let checkpoint = await options.checkpointStore?.get(checkpointKey);
776
+ if (!checkpoint || !checkpointMatchesContext(checkpoint, context)) {
777
+ let receipt;
778
+ try {
779
+ const observation = await inspectWithRetry(inspect, context, maxInspectionAttempts, inspectionRetryBaseDelayMs);
780
+ receipt = buildReportingReceipt(context, observation);
781
+ } catch (error) {
782
+ if (!(error instanceof ReportingInspectionError) || error.retryable || !error.observation) throw error;
783
+ receipt = buildReportingReceipt(context, error.observation);
784
+ if (receipt.status !== "rejected") throw error;
785
+ }
786
+ checkpoint = {
787
+ receipt,
788
+ receiptSyncIdempotencyKey: generateIdempotencyKey(),
789
+ contextFingerprint: checkpointContextFingerprint(context)
790
+ };
791
+ await options.checkpointStore?.put(checkpointKey, checkpoint);
792
+ }
793
+ newReceipts.push(checkpoint.receipt);
794
+ pendingSubmissions.push({ receipt: checkpoint.receipt, idempotencyKey: checkpoint.receiptSyncIdempotencyKey });
795
+ }
796
+ for (const submission of pendingSubmissions) {
797
+ const receiptDeadline = Date.now() + (options.ledgerLimits?.maxLoadMs ?? 6e4);
798
+ const response = await callBeforeDeadline(
799
+ (signal) => options.client.syncReportingReceipts(
800
+ {
801
+ account: options.request.account,
802
+ idempotency_key: submission.idempotencyKey,
803
+ receipts: [submission.receipt]
804
+ },
805
+ { signal }
806
+ ),
807
+ receiptDeadline,
808
+ "RECEIPT_WRITE_FAILED",
809
+ "sync_reporting_receipts exceeded the reporting request deadline"
810
+ );
811
+ const results = response.status === "completed" && Array.isArray(response.results) ? response.results : [];
812
+ const result = results[0];
813
+ const acknowledgedReceipt = result?.receipt;
814
+ const withoutReceivedAt = (receipt) => {
815
+ const { received_at: _receivedAt, ...immutable } = receipt;
816
+ return immutable;
817
+ };
818
+ if (results.length !== 1 || !result || !["recorded", "unchanged"].includes(result.result ?? "") || !acknowledgedReceipt || !same(withoutReceivedAt(acknowledgedReceipt), withoutReceivedAt(submission.receipt)))
819
+ throw new ReportingReconciliationError(
820
+ "RECEIPT_WRITE_FAILED",
821
+ "seller did not return one matching successful receipt acknowledgement"
822
+ );
823
+ }
824
+ if (pendingSubmissions.length) {
825
+ ledger = await loadReportingLedger(
826
+ options.client,
827
+ options.request,
828
+ options.maxSnapshotRestarts,
829
+ options.ledgerLimits
830
+ );
831
+ }
832
+ return {
833
+ ...evaluateReportingLedger(ledger, options.expectedPeriods, options.now),
834
+ submittedReceipts: newReceipts
835
+ };
836
+ }
837
+ export {
838
+ ReportingReconciliationError,
839
+ buildReportingReceipt,
840
+ evaluateReportingLedger,
841
+ isReportingCoverageEvidence,
842
+ loadReportingLedger,
843
+ reconcileReporting
844
+ };