@adcp/sdk 15.7.0 → 15.8.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 (74) hide show
  1. package/dist/lib/core/SingleAgentClient.d.mts +14 -0
  2. package/dist/lib/core/SingleAgentClient.d.ts +14 -0
  3. package/dist/lib/core/SingleAgentClient.js +138 -54
  4. package/dist/lib/core/SingleAgentClient.mjs +141 -54
  5. package/dist/lib/core/agent-discovery-cache.d.mts +2 -0
  6. package/dist/lib/core/agent-discovery-cache.d.ts +2 -0
  7. package/dist/lib/core/creative-schema-support.d.mts +3 -0
  8. package/dist/lib/core/creative-schema-support.d.ts +3 -0
  9. package/dist/lib/core/creative-schema-support.js +113 -0
  10. package/dist/lib/core/creative-schema-support.mjs +89 -0
  11. package/dist/lib/core/product-cache.js +4 -3
  12. package/dist/lib/core/product-cache.mjs +4 -3
  13. package/dist/lib/reporting/ledger/handler.d.mts +10 -3
  14. package/dist/lib/reporting/ledger/handler.d.ts +10 -3
  15. package/dist/lib/reporting/ledger/handler.js +17 -4
  16. package/dist/lib/reporting/ledger/handler.mjs +17 -4
  17. package/dist/lib/reporting/ledger/managed.d.mts +3 -1
  18. package/dist/lib/reporting/ledger/managed.d.ts +3 -1
  19. package/dist/lib/reporting/ledger/managed.js +4 -1
  20. package/dist/lib/reporting/ledger/managed.mjs +4 -1
  21. package/dist/lib/reporting/ledger/postgres.d.mts +87 -0
  22. package/dist/lib/reporting/ledger/postgres.d.ts +87 -0
  23. package/dist/lib/reporting/ledger/postgres.js +314 -13
  24. package/dist/lib/reporting/ledger/postgres.mjs +314 -13
  25. package/dist/lib/reporting/ledger/row-storage.d.mts +23 -0
  26. package/dist/lib/reporting/ledger/row-storage.d.ts +23 -0
  27. package/dist/lib/reporting/ledger/row-storage.js +56 -0
  28. package/dist/lib/reporting/ledger/row-storage.mjs +56 -0
  29. package/dist/lib/reporting/ledger/types.d.mts +26 -0
  30. package/dist/lib/reporting/ledger/types.d.ts +26 -0
  31. package/dist/lib/reporting/service/index.d.mts +33 -3
  32. package/dist/lib/reporting/service/index.d.ts +33 -3
  33. package/dist/lib/reporting/service/index.js +20 -2
  34. package/dist/lib/reporting/service/index.mjs +20 -2
  35. package/dist/lib/reporting/service/maintenance.d.mts +20 -1
  36. package/dist/lib/reporting/service/maintenance.d.ts +20 -1
  37. package/dist/lib/reporting/service/maintenance.js +25 -2
  38. package/dist/lib/reporting/service/maintenance.mjs +25 -2
  39. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  40. package/dist/lib/testing/storyboard/request-builder.js +14 -16
  41. package/dist/lib/testing/storyboard/request-builder.mjs +14 -16
  42. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  43. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  44. package/dist/lib/types/schemas.generated.js +2542 -2542
  45. package/dist/lib/types/schemas.generated.mjs +2542 -2542
  46. package/dist/lib/v2/projection/creative-delivery.d.mts +22 -0
  47. package/dist/lib/v2/projection/creative-delivery.d.ts +22 -0
  48. package/dist/lib/v2/projection/creative-delivery.js +92 -0
  49. package/dist/lib/v2/projection/creative-delivery.mjs +91 -0
  50. package/dist/lib/v2/projection/legacy-metadata.d.mts +6 -3
  51. package/dist/lib/v2/projection/legacy-metadata.d.ts +6 -3
  52. package/dist/lib/v2/projection/legacy-metadata.js +17 -0
  53. package/dist/lib/v2/projection/legacy-metadata.mjs +15 -0
  54. package/dist/lib/validation/bidding-policy.js +1 -1
  55. package/dist/lib/validation/bidding-policy.mjs +1 -1
  56. package/dist/lib/validation/budget-allocation.js +1 -1
  57. package/dist/lib/validation/budget-allocation.mjs +1 -1
  58. package/dist/lib/validation/get-products-request.js +3 -3
  59. package/dist/lib/validation/get-products-request.mjs +3 -3
  60. package/dist/lib/validation/sync-creatives.js +9 -13
  61. package/dist/lib/validation/sync-creatives.mjs +9 -13
  62. package/dist/lib/version.d.mts +3 -3
  63. package/dist/lib/version.d.ts +3 -3
  64. package/dist/lib/version.js +3 -3
  65. package/dist/lib/version.mjs +3 -3
  66. package/docs/TYPE-SUMMARY.md +2 -2
  67. package/docs/ZOD-SCHEMAS.md +11 -1
  68. package/docs/guides/CREATIVE-DELIVERY.md +16 -0
  69. package/docs/guides/REPORTING-LEDGER-CUTOVER.md +405 -0
  70. package/docs/guides/REPORTING-LEDGER.md +130 -0
  71. package/docs/guides/REPORTING-OPERATIONS.md +12 -0
  72. package/docs/llms.txt +3 -2
  73. package/docs/migration-14.x-rc-worksheet.md +4 -4
  74. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * AdCP SDK library version
3
3
  */
4
- export declare const LIBRARY_VERSION = "15.7.0";
4
+ export declare const LIBRARY_VERSION = "15.8.0";
5
5
  /**
6
6
  * AdCP specification version this library is built for
7
7
  */
@@ -33,10 +33,10 @@ export type AdcpVersion = (typeof COMPATIBLE_ADCP_VERSIONS)[number];
33
33
  * Full version information
34
34
  */
35
35
  export declare const VERSION_INFO: {
36
- readonly library: "15.7.0";
36
+ readonly library: "15.8.0";
37
37
  readonly adcp: "3.2.3";
38
38
  readonly compatibleVersions: readonly ["v2.5", "v2.6", "v3", "3.0.0", "3.0", "3.0.1", "3.0.2", "3.0.3", "3.0.4", "3.0.5", "3.0.6", "3.0.7", "3.0.8", "3.0.9", "3.0.10", "3.0.11", "3.0.12", "3.0.13", "3.0.14", "3.0.15", "3.0.16", "3.0.17", "3.0.18", "3.0.19", "3.0.20", "3.0.21", "3.0.22", "3.0.23", "3.0.24", "3.0.25", "3.1.0", "3.1", "3.1.1", "3.1.2", "3.1.3", "3.1.4", "3.1.5", "3.1.6", "3.1.7", "3.1.8", "3.1.9", "3.1.10", "3.1.11", "3.1.12", "3.1.13", "3.1.14", "3.1.15", "3.1.16", "3.1.17", "3.1.18", "3.1.19", "3.1.20", "3.1.21", "3.1.22", "3.1.23", "3.1.24", "3.2.1", "3.2", "3.2.2", "3.2.3"];
39
- readonly generatedAt: "2026-10-09T19:01:02.354Z";
39
+ readonly generatedAt: "2026-10-11T12:51:13.665Z";
40
40
  };
41
41
  /**
42
42
  * Get the AdCP specification version this library is built for
@@ -31,7 +31,7 @@ __export(version_exports, {
31
31
  toReleasePrecisionVersion: () => toReleasePrecisionVersion
32
32
  });
33
33
  module.exports = __toCommonJS(version_exports);
34
- const LIBRARY_VERSION = "15.7.0";
34
+ const LIBRARY_VERSION = "15.8.0";
35
35
  const ADCP_VERSION = "3.2.3";
36
36
  const ADCP_MAJOR_VERSION = 3;
37
37
  const COMPATIBLE_ADCP_VERSIONS = [
@@ -97,10 +97,10 @@ const COMPATIBLE_ADCP_VERSIONS = [
97
97
  "3.2.3"
98
98
  ];
99
99
  const VERSION_INFO = {
100
- library: "15.7.0",
100
+ library: "15.8.0",
101
101
  adcp: "3.2.3",
102
102
  compatibleVersions: COMPATIBLE_ADCP_VERSIONS,
103
- generatedAt: "2026-10-09T19:01:02.354Z"
103
+ generatedAt: "2026-10-11T12:51:13.665Z"
104
104
  };
105
105
  function getAdcpVersion() {
106
106
  return ADCP_VERSION;
@@ -1,4 +1,4 @@
1
- const LIBRARY_VERSION = "15.7.0";
1
+ const LIBRARY_VERSION = "15.8.0";
2
2
  const ADCP_VERSION = "3.2.3";
3
3
  const ADCP_MAJOR_VERSION = 3;
4
4
  const COMPATIBLE_ADCP_VERSIONS = [
@@ -64,10 +64,10 @@ const COMPATIBLE_ADCP_VERSIONS = [
64
64
  "3.2.3"
65
65
  ];
66
66
  const VERSION_INFO = {
67
- library: "15.7.0",
67
+ library: "15.8.0",
68
68
  adcp: "3.2.3",
69
69
  compatibleVersions: COMPATIBLE_ADCP_VERSIONS,
70
- generatedAt: "2026-10-09T19:01:02.354Z"
70
+ generatedAt: "2026-10-11T12:51:13.665Z"
71
71
  };
72
72
  function getAdcpVersion() {
73
73
  return ADCP_VERSION;
@@ -1,7 +1,7 @@
1
1
  # AdCP Type Summary
2
2
 
3
- > Generated at: 2026-10-09
4
- > @adcp/sdk v15.7.0
3
+ > Generated at: 2026-10-11
4
+ > @adcp/sdk v15.8.0
5
5
 
6
6
  Curated reference of the types that matter for using the AdCP client. For full generated types see `src/lib/types/tools.generated.ts` and `src/lib/types/core.generated.ts`.
7
7
 
@@ -30,7 +30,17 @@ import { EventSourceHealthSchema } from '@adcp/sdk/schemas/browser';
30
30
  const health = EventSourceHealthSchema.parse(rawHealth);
31
31
  ```
32
32
 
33
- This entry has no Node built-ins in its import graph and shares the Node entry's portable validators, including canonical and legacy get-products selectors. JSON Schema loading and tool registration helpers are available from `@adcp/sdk/schemas`. Importing one schema can still retain the full generated module; per-schema tree-shaking is tracked in [#3178](https://github.com/adcontextprotocol/adcp-client/issues/3178).
33
+ This entry has no Node built-ins in its import graph and shares the Node entry's portable validators, including canonical and legacy get-products selectors. Named ESM imports let bundlers remove unused validators while preserving the selected schema's dependencies. JSON Schema loading and tool registration helpers are available from `@adcp/sdk/schemas`.
34
+
35
+ `npm run test:schemas-browser` bundles a single `EventSourceHealthSchema` import with esbuild (`platform: 'browser'`, `format: 'iife'`, `target: 'es2022'`, `minify: true`). The standalone bundle includes Zod and ajv-formats, with no external dependencies. Measurements for esbuild 0.28.2 and Zod 4.4.3:
36
+
37
+ | Measure | Before tree-shaking | With pure schema initializers | CI maximum |
38
+ | --- | ---: | ---: | ---: |
39
+ | Minified bundle | 1,539,250 B | 336,182 B | 375,000 B |
40
+ | Gzipped bundle | 265,418 B | 67,420 B | 75,000 B |
41
+ | Generated validators retained | 1,203,529 B | 744 B | 2,048 B |
42
+
43
+ The total-size limits leave roughly 11% headroom for dependency changes. The separate generated-code limit catches unrelated validators being retained even if dependencies become smaller. The check also verifies that product, media-buy, and reporting-obligation validators disappear and that the bundled schema still validates without Node globals. Existing tests exercise all exports through ESM and CommonJS, declaration paths, and portable cross-field validation.
34
44
 
35
45
  ## Why Zod Schemas?
36
46
 
@@ -135,6 +135,12 @@ tool is never reused. Unknown peers receive a legacy projection only when the
135
135
  selected product provides one unambiguous ref. Ambiguous or unavailable mappings throw
136
136
  `CreativeFormatProjectionError` before transport.
137
137
 
138
+ Schema inspection follows local JSON Pointer `$ref` targets, including `$defs`,
139
+ and retains the complete listed document through shared discovery-cache reuse.
140
+ Unused definitions and annotations do not establish support. External references,
141
+ named anchors, and dynamic references are not resolved; inspection never fetches
142
+ schema URLs. Cycles and traversal limits cannot force a guessed wire mode.
143
+
138
144
  Before building creatives, ask the client which mode that write will use:
139
145
 
140
146
  ```ts
@@ -181,6 +187,16 @@ including polling, deferred, and webhook completions. A creative assigned to
181
187
  multiple packages downgrades only when every package resolves to the same
182
188
  legacy ref set.
183
189
 
190
+ For a canonical seller whose product declares only legacy `format_ids`, those
191
+ authored option refs name IDs minted locally by the SDK. After discovering the
192
+ product with the same client, create/update writes send the equivalent
193
+ `format_kind` + `params` selector and sync writes drop undeclared creative pins.
194
+ Product-cache snapshots retain this seller-origin evidence privately. A selection
195
+ that needs multiple direct selectors, or an undeclared custom format that needs
196
+ its schema identity, fails with `ADCP_CREATIVE_FORMAT_PROJECTION_FAILED` before
197
+ dispatch. Seller-declared option refs remain unchanged. Re-run `getProducts()`
198
+ after restarting before writing selections derived from legacy-only products.
199
+
184
200
  After a process restart, re-run product and media-buy reads that carry enough
185
201
  selector context or configure the explicit canonical-to-legacy resolver below.
186
202
  An accountless write never consumes a tenant-scoped cached package route based
@@ -0,0 +1,405 @@
1
+ # Cutting over to the SDK reporting ledger from your own ledger tables
2
+
3
+ This guide is for a seller that already runs its own Reliable Reporting ledger
4
+ (tables for configuration generations, obligations and revisions, row files in
5
+ object storage and a warehouse, status issues and transitions) and wants
6
+ `@adcp/sdk` to own the ledger from now on. Read the
7
+ [ledger guide](REPORTING-LEDGER.md) first for the SDK side, and the
8
+ [operations runbook](REPORTING-OPERATIONS.md) for production practice.
9
+
10
+ The short version:
11
+
12
+ - Pick **one authoritative ledger per period**. The SDK ledger produces and
13
+ serves every period from a boundary you choose. Your tables become read-only.
14
+ - **Do not migrate data and do not dual-write.** The old tables keep answering
15
+ exact reads for old revision IDs until their retention lapses, through a
16
+ single read-only hook, then you drop them.
17
+ - Everything buyer-visible stays stable across the cutover: old revision IDs
18
+ keep resolving, and new ones come from the SDK.
19
+
20
+ ## Adopt the SDK tables, or implement the store over your own?
21
+
22
+ You have two ways to run the SDK's service on top of what you have.
23
+
24
+ | | Adopt `PostgresReportingLedgerStore` (recommended) | Implement `ReportingLedgerStore` over your tables |
25
+ | --- | --- | --- |
26
+ | Schema | The SDK's `adcp_reporting_*` tables, installed by `REPORTING_LEDGER_MIGRATION` and friends | Yours |
27
+ | Row storage | `rowStorage`: verified chunked rows in your GCS, S3 or Azure bucket, or PostgreSQL | You implement `readRevisionRows` and verify rows against the committed binding yourself |
28
+ | Change feed | `changeFeed: true`: `changesAfter`, current-revision reads, consumer cursors | You implement it, or go without host reads and the warehouse sink |
29
+ | Retention | `maintenance.retention`: period-aligned retirement honouring the change-feed hold | You implement `retireExpiredPeriods` and tombstones, or never retire |
30
+ | Maintenance | `maintenance`: snapshot sweep, abandoned-upload sweep, feed pruning | You write all of it |
31
+ | Warehouse | `@adcp/sdk/reporting/bigquery` sink | Not available: it reads the SDK store |
32
+ | Correctness | Lease fencing, gap-free status checkpoints, immutable generations and the revision binding are enforced in one tested place | Every one of those guarantees is yours to prove |
33
+ | Existing data | Starts empty; old tables stay behind a read-only hook | Old data served natively |
34
+
35
+ Implementing the store over your own tables is a large surface: the store is
36
+ the correctness boundary for leases, fencing, immutability, snapshot
37
+ checkpoints and the binding digest. You also forgo row storage, the change
38
+ feed, retention and the sink, which are all implemented against the SDK
39
+ tables. Choose it only when a hard constraint forbids a second schema. For
40
+ almost every seller with an existing ledger, adopt the SDK tables and put the
41
+ old ones behind the [legacy fallback](#the-legacy-fallback-hook).
42
+
43
+ ## Core-only setup with GCS object rows
44
+
45
+ Run the SDK ledger in its own schema or database. The default table set is
46
+ deployment-wide, so the store requires `acknowledgeIsolatedDatabase: true`.
47
+ You do not need the production service: Core plus `maintenance` covers
48
+ production, serving, row storage, the change feed and retention. If you
49
+ advertise Managed Delivery or Reconciled Billing today, read
50
+ [What a Core-only cutover does not carry](#what-a-core-only-cutover-does-not-carry).
51
+
52
+ ```ts
53
+ import { Pool } from 'pg';
54
+ import { Storage } from '@google-cloud/storage';
55
+ import { PostgresReportingLedgerStore } from '@adcp/sdk/reporting/ledger';
56
+ import { createGcsReportingRowObjectProviderV1 } from '@adcp/sdk/reporting/gcs';
57
+ import { createReliableReportingService } from '@adcp/sdk/reporting/service';
58
+
59
+ // Apply once, in order, with your migration owner (PostgreSQL 13+):
60
+ // REPORTING_LEDGER_MIGRATION, REPORTING_ROW_STORAGE_MIGRATION,
61
+ // REPORTING_LEDGER_CHANGES_MIGRATION
62
+
63
+ const pool = new Pool({ connectionString: process.env.REPORTING_LEDGER_DATABASE_URL });
64
+ const store = new PostgresReportingLedgerStore(pool, {
65
+ acknowledgeIsolatedDatabase: true,
66
+ changeFeed: true,
67
+ rowStorage: {
68
+ deploymentNamespace: 'prod-us',
69
+ bindings: {
70
+ rows: {
71
+ kind: 'object',
72
+ provider: 'gcs',
73
+ location: { bucket: 'acme-reporting' }, // your existing bucket
74
+ prefix: 'adcp-ledger-rows', // a prefix the legacy writer never uses
75
+ compression: 'gzip',
76
+ },
77
+ },
78
+ providers: { gcs: createGcsReportingRowObjectProviderV1({ storage: new Storage() }) },
79
+ selectRowBinding: () => 'rows',
80
+ },
81
+ });
82
+ await store.readyRowStorage();
83
+
84
+ const reporting = createReliableReportingService({
85
+ store,
86
+ statusRetentionDays: 90, // the commitment advertised to buyers; retention uses it
87
+ // ...adapters, contact, resolveSource, resolveCurrency, resolveCoverage
88
+ legacyRevisionReader, // see "The legacy fallback hook"
89
+ maintenance: {
90
+ retention: { enabled: true, recordRetentionDays: 120, limit: 100 },
91
+ changeFeed: { changeRetentionDays: 30, maxFeedHoldDays: 7 },
92
+ },
93
+ });
94
+ ```
95
+
96
+ With object row storage, PostgreSQL holds only revision headers, row-set and
97
+ chunk manifests (digests, sizes, object keys and versions) and lifecycle
98
+ state. The bytes live in your bucket.
99
+
100
+ - **Reusing your bucket.** Point the binding at the existing bucket with a
101
+ prefix the legacy writer never touches. Keys are `{prefix}/{namespace_key}/...`
102
+ with a hashed account segment, and the SDK only creates, reads and deletes
103
+ objects under that prefix.
104
+ - **Bucket requirements.** Uniform bucket-level access and enforced public
105
+ access prevention are required. A lifecycle `Delete` rule that can match live
106
+ objects under the SDK prefix (no `matchesPrefix`, or an overlapping one) makes
107
+ the GCS probe refuse the binding, so scope any bucket-wide lifecycle rule to
108
+ your legacy prefix. Keep retention-driven deletion in the ledger's pruning.
109
+ - **Namespace.** `deploymentNamespace` is mixed into every key. Keep it stable
110
+ across deploys and distinct per environment.
111
+ - **Maintenance.** `reporting.start({ deploymentWide: true, onError })` runs
112
+ `maintenance` at most once per `intervalMilliseconds`; or call
113
+ `reporting.runMaintenance()` from your own scheduler. Retention uses the
114
+ service's `statusRetentionDays`, so it can never retire a period earlier than
115
+ the retention you advertise. See
116
+ [Core-only setup](REPORTING-LEDGER.md#core-only-setup-with-row-storage-change-feed-and-retention).
117
+ Enable `retention` last.
118
+
119
+ ### Keeping your BigQuery tables fed
120
+
121
+ `@adcp/sdk/reporting/bigquery` follows the change feed and loads every
122
+ committed revision's verified rows into BigQuery tables you name
123
+ ([sink details](REPORTING-LEDGER.md#bigquery-warehouse-sink)). The warehouse
124
+ copy is derived and never served.
125
+
126
+ ```ts
127
+ const sink = createBigQueryReportingWarehouseSinkV1({
128
+ store,
129
+ db: pool,
130
+ bigquery: new BigQuery({ projectId: 'analytics-prod' }),
131
+ name: 'warehouse',
132
+ projectId: 'analytics-prod',
133
+ datasetId: 'reporting',
134
+ rowsTableId: 'media_buy_delivery_rows_v2', // new tables, not your legacy ones
135
+ revisionsTableId: 'media_buy_delivery_revisions_v2',
136
+ mapRow: row => ({ media_buy_id: row.media_buy_id, impressions: row.impressions, spend: row.spend }),
137
+ });
138
+ ```
139
+
140
+ Give the sink its **own new tables** rather than your legacy ones. It adds its
141
+ own fixed columns (`reporting_revision_id`, `reporting_obligation_id`,
142
+ `account_id`, `revision_number`, `finality`, `period_date`, `ordinal`), and
143
+ loading into a table another writer owns invites double counting. Give analysts
144
+ one view that `UNION ALL`s the legacy table, bounded to periods before the
145
+ cutover boundary, with the sink's `current_rows` view, bounded to periods from
146
+ it. Query `current_rows`, never the raw rows table, because pacing snapshots
147
+ are cumulative.
148
+
149
+ ## Cutover, step by step
150
+
151
+ Choose a **cutover boundary** `B`: the start of a reporting period (for a daily
152
+ schedule, a midnight in the source timezone). Periods that start before `B`
153
+ belong to your legacy ledger forever. Periods that start at or after `B`
154
+ belong to the SDK ledger.
155
+
156
+ 1. **Stand up the SDK ledger without serving it.** Apply the migrations, build
157
+ the store and service, run `runMaintenance()`, and confirm the GCS probe
158
+ passes in each environment. Do not call `reporting.install(platform)` yet
159
+ and do not start its scheduler. Nothing is buyer-visible.
160
+ 2. **Wire the legacy fallback hook** (below) over your existing exact-revision
161
+ read, and exercise it in staging with a revision ID from your legacy tables.
162
+ 3. **Rehearse in staging** with a shadow service (see
163
+ [Shadow comparison](#shadow-comparison)) over a few real periods until row
164
+ counts, control totals and row digests match.
165
+ 4. **Install configurations in the last legacy period.** The SDK plans
166
+ obligations from `max(schedule.anchor, installedAt)`, rounded up to the next
167
+ period boundary. Install each active configuration through
168
+ `installConfiguration` (your `accounts.upsert` mapping) after the start of
169
+ the last legacy period and before `B`, with `schedule.anchor` no later than
170
+ `B`. The first period the SDK produces is then `B`, never the period in
171
+ progress. Verify that no SDK obligation exists for a period before `B`.
172
+ 5. **Stop the legacy writer for periods at or after `B`.** The legacy
173
+ production job must not plan or write any period starting at or after `B`,
174
+ and must not publish new revisions for earlier ones (see
175
+ [Corrections to pre-cutover periods](#corrections-to-pre-cutover-periods)).
176
+ Do this by configuration (disable the planner for the new periods), not by
177
+ revoking database permissions mid-write.
178
+ 6. **Switch the serving path.** Deploy with `reporting.install(platform)` and
179
+ `reporting.start({ deploymentWide: true, onError })`. From this deploy the
180
+ SDK's handlers answer `get_reporting_status` and `get_media_buy_delivery`,
181
+ the capability document is the SDK's, and exact reads for old revision IDs
182
+ flow through the fallback. Make the legacy tables read-only now (revoke
183
+ `INSERT`, `UPDATE` and `DELETE` from the application role).
184
+ 7. **Watch the first SDK period close:** worker progress, the first revision's
185
+ binding, a buyer exact read, the warehouse sink's `runOnce()` result, and a
186
+ legacy-ID exact read through the fallback.
187
+ 8. **Decommission** the legacy tables after retention
188
+ ([below](#decommissioning-the-old-tables)).
189
+
190
+ Because the boundary is a period boundary, no period is ever produced by both
191
+ ledgers, so no revision ID or `revision_content_sha256` is ever ambiguous.
192
+
193
+ ### What `get_reporting_status` shows after cutover
194
+
195
+ History starts at the cutover. The SDK ledger knows only the periods it owns,
196
+ from the first boundary at or after each configuration's install instant. A
197
+ status read over a wide range returns the SDK's obligations, revisions and
198
+ health for periods at or after `B` and nothing for earlier periods. Those
199
+ periods are outside the SDK's ownership, so they are absent rather than
200
+ reported as missing history. `HISTORY_UNAVAILABLE` is reserved for periods the
201
+ SDK owns whose obligation records are gone (retired by retention or
202
+ compacted).
203
+
204
+ Buyers that need pre-cutover data keep using the revision IDs they already
205
+ hold: exact `get_media_buy_delivery` reads for those IDs are served by the
206
+ fallback for as long as your legacy tables retain them. Tell buyers that status
207
+ listing starts at the boundary while exact reads of known revisions continue.
208
+
209
+ The SDK has no hook for listing pre-cutover periods through
210
+ `get_reporting_status`. Do not synthesize SDK obligations for old periods: that
211
+ is a data migration, with all the identity problems described below.
212
+
213
+ ### Corrections to pre-cutover periods
214
+
215
+ A late correction to a period that started before `B` needs a decision. The
216
+ SDK cannot produce a revision for a period it never owned, and your legacy
217
+ writer is read-only. Either keep the legacy writer enabled **for corrections of
218
+ pre-cutover periods only**, and keep those tables writable until the last such
219
+ period leaves its correction window, or handle them out of band. The rule that
220
+ must hold is that every period has exactly one authoritative writer.
221
+
222
+ ## The legacy fallback hook
223
+
224
+ `createReliableReportingService` (and
225
+ `createPostgresReliableReportingProductionService`) takes an optional
226
+ `legacyRevisionReader`. The SDK handler always answers first. Only when the SDK
227
+ ledger has no such revision for the resolved account does it call your reader
228
+ with the same request and context.
229
+
230
+ ```ts
231
+ import type { ReportingLegacyRevisionReaderV1 } from '@adcp/sdk/reporting/ledger';
232
+
233
+ const legacyRevisionReader: ReportingLegacyRevisionReaderV1 = async (request, context) => {
234
+ // `context.account` is the framework-resolved account, not request.account.
235
+ const accountId = (context.account as { id: string }).id;
236
+ const row = await legacyDb.findRevision({
237
+ reporting_revision_id: request.reporting_revision_id,
238
+ account_id: accountId, // scope by the resolved account
239
+ });
240
+ if (!row) return null; // not found here either
241
+ return buildGetMediaBuyDeliveryResponse(row, request.pagination);
242
+ };
243
+ ```
244
+
245
+ The contract:
246
+
247
+ - **Signature.** `(request, context) => Promise<GetMediaBuyDeliveryResponse | null | undefined>`.
248
+ The request and context are exactly what the SDK handler received, including
249
+ `pagination` and `context.signal`.
250
+ - **Not found.** Return `null` or `undefined`. The caller gets the SDK's own
251
+ nondisclosing `REFERENCE_NOT_FOUND` with `field: "reporting_revision_id"`,
252
+ identical to an unknown ID on a pure-SDK deployment.
253
+ - **Errors.** An error your reader throws propagates unchanged. Throw
254
+ `AdcpError('SERVICE_UNAVAILABLE')` for your own outages instead of returning
255
+ `null`, so buyers retry instead of concluding the revision is gone.
256
+ - **Authorization is yours.** The SDK has resolved the account and cannot judge
257
+ whether a legacy revision belongs to it. Your reader **must** scope the lookup
258
+ by `context.account` and return `null` for another account's revision. The SDK
259
+ also routes a request for an SDK-owned revision that belongs to a *different*
260
+ account to your reader, so it must never answer for a revision it cannot
261
+ attribute to the resolved account.
262
+ - **When it is called.** Only for a revision the SDK ledger does not hold for
263
+ the resolved account.
264
+ - **When it is never called.** For an SDK-owned revision that is expired, whose
265
+ rows are missing, corrupt or disagree with the committed binding, or that
266
+ comes with an unusable cursor; and on read capacity exhaustion or any other
267
+ error. Falling back there could serve different content under an ID the SDK
268
+ owns.
269
+ - **Read-only and bounded.** The hook must not write to the SDK ledger. The SDK
270
+ releases its per-account read slot before calling you, so bound your reader's
271
+ own concurrency and timeouts.
272
+ - **Shape.** Return a full `get_media_buy_delivery` response for that revision,
273
+ including `reporting_revision_binding` and `reporting_revision` with the
274
+ revision ID and `revision_content_sha256` your buyers already recorded. Page
275
+ rows with the same `pagination` contract; your cursors only need to round-trip
276
+ through your reader.
277
+ - **Identity.** Keep legacy revision IDs distinguishable from SDK IDs (`rrev_`
278
+ plus a 32-character digest) so a legacy ID can never collide with one the SDK
279
+ later derives.
280
+
281
+ ## Why not dual-write
282
+
283
+ A second authoritative ledger looks like a safe migration path and is the most
284
+ dangerous one.
285
+
286
+ - **Revision IDs and content hashes are buyer-visible.** The SDK derives a
287
+ revision ID from the obligation and the source publication, and
288
+ `revision_content_sha256` is the SHA-256 of the canonical JSON of
289
+ `{ reporting_revision_id, row_count, control_totals, reporting_rows }`. Two
290
+ ledgers writing the same period produce **two different IDs and two different
291
+ hashes**, and the buyer's receipts bind to whichever one they saw.
292
+ - **Divergence is silent.** Two writers can disagree on a restatement, an
293
+ official finality decision or an adjustment, and nothing detects it until a
294
+ buyer reconciles a bill against a revision the other ledger never recorded.
295
+ - **Immutability is per ledger.** An official revision is terminal in one
296
+ ledger while the other may still treat the period as open.
297
+ - **Leases and fencing do not span ledgers.** Each fences only its own writers.
298
+
299
+ A **shadow write for comparison is fine** as long as it is never served: a
300
+ separate database, never installed on the platform, never advertised, never
301
+ notifying. The moment a shadow ledger can answer a buyer, it is a second
302
+ authoritative ledger.
303
+
304
+ ## Shadow comparison
305
+
306
+ Before the boundary, run the SDK service against a separate shadow database and
307
+ feed it the same source periods. Compare what the two ledgers committed for the
308
+ same period. Do not compare `revision_content_sha256` directly: it includes the
309
+ revision ID, and IDs differ between ledgers by construction. Compare the inputs
310
+ to that hash:
311
+
312
+ ```ts
313
+ import { createHash } from 'node:crypto';
314
+ import { canonicalize } from '@adcp/sdk';
315
+
316
+ const rowsDigest = (rows: readonly unknown[]) => createHash('sha256').update(canonicalize(rows), 'utf8').digest('hex');
317
+
318
+ // SDK side, against the shadow store
319
+ const current = await shadowStore.getCurrentRevision({ account_id, reporting_obligation_id });
320
+ const page = await shadowStore.readRevisionRows({
321
+ reporting_revision_id: current.reporting_revision_id,
322
+ account_id,
323
+ offset: 0,
324
+ limit: Math.max(1, current.binding.rowCount),
325
+ });
326
+
327
+ // Compare with the legacy revision for the same account and period:
328
+ // row count, control totals (name and exact decimal value), rowsDigest(rows)
329
+ ```
330
+
331
+ - **Parity means** equal row count, equal `control_totals`, and equal canonical
332
+ row digests. Array order is part of the canonical form, so sort both sides by
333
+ your natural key first if your legacy writer does not guarantee order.
334
+ - **Expected differences:** revision ID, `revision_content_sha256`, timestamps,
335
+ freshness and manifest evidence.
336
+ - Run the comparison until a full set of periods, including one restatement,
337
+ matches. Then tear the shadow down; do not leave a second ledger running.
338
+
339
+ ## Decommissioning the old tables
340
+
341
+ After cutover the legacy tables answer exact reads for revision IDs buyers
342
+ already hold. The retention you advertised bounds how long.
343
+
344
+ 1. The last legacy period ends at `B`. Keep its revisions for the legacy
345
+ deployment's advertised `status_retention_days`, counted from that close.
346
+ 2. When that window has lapsed for every legacy period, the fallback has
347
+ nothing buyers can still legitimately ask for. Remove `legacyRevisionReader`,
348
+ deploy, and confirm an exact read of an old ID returns `REFERENCE_NOT_FOUND`.
349
+ 3. Stop the legacy export jobs, drop the legacy tables, delete or archive the
350
+ legacy objects (under their own prefix, never under the SDK prefix), and
351
+ retire the legacy BigQuery tables once analysts have moved to the new view.
352
+
353
+ Keep the legacy tables for the whole window, not only until the last revision
354
+ was read: buyers verify revisions up to the advertised retention, and that value
355
+ is a commitment. The SDK's `statusRetentionDays` applies to new periods only.
356
+
357
+ ## What a Core-only cutover does not carry
358
+
359
+ Core-only advertises Reliable Reporting Core: `get_reporting_status` and
360
+ `get_media_buy_delivery`. It does not deliver files to buyer destinations
361
+ (Managed Delivery) and does not accept consumer receipts (Reconciled Billing).
362
+ If your legacy materializations feed buyer-owned destinations, either keep
363
+ delivering them from your own pipeline, reading revisions through
364
+ `listCurrentRevisions` or the change feed rather than from your old tables, or
365
+ adopt the production service with the managed-delivery add-on as a separate,
366
+ later step. Do not advertise a tier the new ledger does not implement.
367
+
368
+ ## Mapping from typical custom tables to the SDK tables
369
+
370
+ Your names will differ. Map by role.
371
+
372
+ | Your concept (typical custom table) | SDK table | Notes |
373
+ | --- | --- | --- |
374
+ | Configuration generation (`reporting_core_config_generation`) | `adcp_reporting_configurations` | One immutable generation per `(account_id, delivery_config_id, delivery_config_version)`, with a semantic fingerprint and the frozen source scope and currency in `data` (JSONB). Installed by `installConfiguration`; a changed configuration is a new generation. |
375
+ | Obligation (`reporting_obligation`) | `adcp_reporting_obligations` | One row per configuration and period ordinal: period bounds, `state` (`pending` or `terminal`), `next_attempt_at`, lease owner, generation and expiry. The rest is in `data`. |
376
+ | Revision with explicit columns (`reporting_revision`, 39 columns) | `adcp_reporting_revisions` | Promoted columns are `revision_id`, `obligation_id`, `revision_number`, `finality`, `kind`, `supersedes_revision_id` and `content_sha256`. The remainder (report definition and digest, profile, schema version, row count, control totals, freshness, manifest and finality evidence) is the wire revision inside `data` (JSONB). Do not recreate your 39 columns: use `getCurrentRevision`, `listCurrentRevisions` and the change feed. |
377
+ | Row files (GCS objects plus BigQuery) | `adcp_reporting_row_bindings`, `adcp_reporting_row_sets`, `adcp_reporting_row_chunks`, plus your bucket | Postgres holds the manifest only: digests, sizes, object key and native version per chunk. Rows are verified canonical JSONL chunks in the object store. An unchanged pulse shares its predecessor's row set. BigQuery is fed by the sink, not by the ledger. |
378
+ | Corrections after an official revision | `adcp_reporting_adjustments` | Immutable adjustments bound to the official revision, never superseding revisions. |
379
+ | Status issues | `adcp_reporting_issues` | Obligation-scoped issues with observed and resolved times. |
380
+ | Status transitions and lifecycle | `adcp_reporting_transitions`, `adcp_reporting_lifecycle_state` | Append-only health transitions with a sequence and a durable notified marker, plus a per-obligation reconciliation watermark. Current health is derived from these on read. |
381
+ | Materializations (delivery evidence for buyer destinations) | `adcp_reporting_materializations`, `adcp_reporting_managed_bindings`, `adcp_reporting_receipts` | Managed Delivery add-on only; absent in a Core-only deployment. |
382
+ | Snapshots as status read caches | `adcp_reporting_snapshots`, `adcp_reporting_checkpoints` | Ephemeral cursor state with a 15-minute expiry, swept by maintenance. Not a durable record. |
383
+ | Snapshots as intraday revisions | `adcp_reporting_revisions` with `finality = 'snapshot'` | A pacing snapshot is a revision whose finality is `snapshot`; `official` is terminal. |
384
+ | Retired periods | `adcp_reporting_obligation_tombstones` | A compact record that refuses replays and preserves the audit conclusion after retention deletes a period. |
385
+ | Change log | `adcp_reporting_changes`, `adcp_reporting_feed_consumers` (plus horizons) | With `changeFeed: true`. Replaces polling your revision table. |
386
+ | Row upload bookkeeping | `adcp_reporting_object_write_*` | Intent and fencing records for object uploads. |
387
+
388
+ ## Cutover checklist
389
+
390
+ - [ ] SDK ledger in an isolated schema or database, migrations applied, probes
391
+ green in every environment.
392
+ - [ ] Row bucket prefix is separate from legacy objects; no bucket lifecycle rule
393
+ can delete live SDK objects.
394
+ - [ ] `legacyRevisionReader` scopes by `context.account`, returns `null` for
395
+ unknown, and throws for its own outages.
396
+ - [ ] Cutover boundary `B` chosen; each configuration installed during the last
397
+ legacy period; no SDK obligation exists before `B`.
398
+ - [ ] Legacy writer disabled for periods at or after `B` (and a correction path
399
+ for earlier periods decided).
400
+ - [ ] Legacy tables read-only after the switch deploy.
401
+ - [ ] Buyers told: status listing starts at `B`; old revision IDs keep working
402
+ until the legacy retention lapses.
403
+ - [ ] Shadow ledger (if any) is unreachable from the serving path.
404
+ - [ ] Decommission date recorded: end of the last legacy period plus the legacy
405
+ `status_retention_days`.