@adcp/sdk 15.7.1 → 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.
- package/dist/lib/core/SingleAgentClient.d.mts +14 -0
- package/dist/lib/core/SingleAgentClient.d.ts +14 -0
- package/dist/lib/core/SingleAgentClient.js +138 -54
- package/dist/lib/core/SingleAgentClient.mjs +141 -54
- package/dist/lib/core/agent-discovery-cache.d.mts +2 -0
- package/dist/lib/core/agent-discovery-cache.d.ts +2 -0
- package/dist/lib/core/creative-schema-support.d.mts +3 -0
- package/dist/lib/core/creative-schema-support.d.ts +3 -0
- package/dist/lib/core/creative-schema-support.js +113 -0
- package/dist/lib/core/creative-schema-support.mjs +89 -0
- package/dist/lib/core/product-cache.js +4 -3
- package/dist/lib/core/product-cache.mjs +4 -3
- package/dist/lib/reporting/ledger/handler.d.mts +10 -3
- package/dist/lib/reporting/ledger/handler.d.ts +10 -3
- package/dist/lib/reporting/ledger/handler.js +17 -4
- package/dist/lib/reporting/ledger/handler.mjs +17 -4
- package/dist/lib/reporting/ledger/managed.d.mts +3 -1
- package/dist/lib/reporting/ledger/managed.d.ts +3 -1
- package/dist/lib/reporting/ledger/managed.js +4 -1
- package/dist/lib/reporting/ledger/managed.mjs +4 -1
- package/dist/lib/reporting/ledger/postgres.d.mts +87 -0
- package/dist/lib/reporting/ledger/postgres.d.ts +87 -0
- package/dist/lib/reporting/ledger/postgres.js +314 -13
- package/dist/lib/reporting/ledger/postgres.mjs +314 -13
- package/dist/lib/reporting/ledger/row-storage.d.mts +23 -0
- package/dist/lib/reporting/ledger/row-storage.d.ts +23 -0
- package/dist/lib/reporting/ledger/row-storage.js +56 -0
- package/dist/lib/reporting/ledger/row-storage.mjs +56 -0
- package/dist/lib/reporting/ledger/types.d.mts +26 -0
- package/dist/lib/reporting/ledger/types.d.ts +26 -0
- package/dist/lib/reporting/service/index.d.mts +33 -3
- package/dist/lib/reporting/service/index.d.ts +33 -3
- package/dist/lib/reporting/service/index.js +20 -2
- package/dist/lib/reporting/service/index.mjs +20 -2
- package/dist/lib/reporting/service/maintenance.d.mts +20 -1
- package/dist/lib/reporting/service/maintenance.d.ts +20 -1
- package/dist/lib/reporting/service/maintenance.js +25 -2
- package/dist/lib/reporting/service/maintenance.mjs +25 -2
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/testing/storyboard/request-builder.js +14 -16
- package/dist/lib/testing/storyboard/request-builder.mjs +14 -16
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/v2/projection/creative-delivery.d.mts +22 -0
- package/dist/lib/v2/projection/creative-delivery.d.ts +22 -0
- package/dist/lib/v2/projection/creative-delivery.js +92 -0
- package/dist/lib/v2/projection/creative-delivery.mjs +91 -0
- package/dist/lib/v2/projection/legacy-metadata.d.mts +6 -3
- package/dist/lib/v2/projection/legacy-metadata.d.ts +6 -3
- package/dist/lib/v2/projection/legacy-metadata.js +17 -0
- package/dist/lib/v2/projection/legacy-metadata.mjs +15 -0
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.mjs +3 -3
- package/docs/TYPE-SUMMARY.md +2 -2
- package/docs/guides/CREATIVE-DELIVERY.md +16 -0
- package/docs/guides/REPORTING-LEDGER-CUTOVER.md +405 -0
- package/docs/guides/REPORTING-LEDGER.md +130 -0
- package/docs/guides/REPORTING-OPERATIONS.md +12 -0
- package/docs/llms.txt +3 -2
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- package/package.json +1 -1
|
@@ -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`.
|
|
@@ -233,6 +233,14 @@ empty revision association. A deployment with per-tenant workers should pass
|
|
|
233
233
|
the resolved `account_id` to both `planObligations()` and `runWorker()`;
|
|
234
234
|
omitting it intentionally runs a deployment-wide worker.
|
|
235
235
|
|
|
236
|
+
### Already running your own ledger tables?
|
|
237
|
+
|
|
238
|
+
Do not dual-write, and do not migrate data. Cut over at a period boundary: the
|
|
239
|
+
SDK ledger produces and serves new periods, and your old tables become
|
|
240
|
+
read-only behind the optional `legacyRevisionReader`, which answers exact
|
|
241
|
+
`get_media_buy_delivery` reads only for revisions the SDK ledger does not hold.
|
|
242
|
+
See [Cutting over to the SDK reporting ledger](REPORTING-LEDGER-CUTOVER.md).
|
|
243
|
+
|
|
236
244
|
### Migrating an existing manual lifecycle
|
|
237
245
|
|
|
238
246
|
Keep the same `PostgresReportingLedgerStore` and run the same
|
|
@@ -373,6 +381,22 @@ await store.readyRowStorage();
|
|
|
373
381
|
segment, never a raw account ID. Override `keyTemplate` with the placeholders
|
|
374
382
|
in `REPORTING_ROW_KEY_PLACEHOLDERS`; templates must include
|
|
375
383
|
`{content_sha256}` and `{chunk_index}`.
|
|
384
|
+
Enabling snapshot compaction additionally requires `{revision_key}` (see
|
|
385
|
+
[Compacting superseded snapshots](#compacting-superseded-snapshots-opt-in)).
|
|
386
|
+
- A custom `keyTemplate` without `{account_key}` and `{revision_key}` can map
|
|
387
|
+
row sets from different obligations or accounts with identical chunk content
|
|
388
|
+
(an adjustment's digest covers its rows alone) to one object, which the
|
|
389
|
+
second writer adopts. Every delete path (period retirement, compaction and
|
|
390
|
+
the intent sweep) therefore skips an object while a live row set outside the
|
|
391
|
+
operation still references the same binding, key and native version, or an
|
|
392
|
+
open write intent created it. The reference check and the delete run in one
|
|
393
|
+
transaction under a per-object advisory lock, and a commit that adopts an
|
|
394
|
+
existing object takes the same lock, re-reads the object and holds the lock
|
|
395
|
+
until its chunk rows commit. A delete that wins the lock makes the adopting
|
|
396
|
+
commit fail with `STATE_UNAVAILABLE` (the writer re-uploads once its intent
|
|
397
|
+
lapses); a commit that wins is seen by the delete, which skips. A skipped
|
|
398
|
+
object is deleted by the retirement, compaction or sweep of the last row set
|
|
399
|
+
that referenced it, so it is not leaked.
|
|
376
400
|
- An upload runs only while the obligation lease still covers
|
|
377
401
|
`writeDeadlineMilliseconds` plus five seconds. A fenced write intent records
|
|
378
402
|
every object the upload created, and the ledger commit closes the intent in
|
|
@@ -546,6 +570,111 @@ because the retired obligations no longer exist.
|
|
|
546
570
|
always sweeps abandoned row uploads and expired cursor snapshots and
|
|
547
571
|
checkpoints, and retires periods when retention is enabled.
|
|
548
572
|
|
|
573
|
+
### Compacting superseded snapshots (opt-in)
|
|
574
|
+
|
|
575
|
+
> **Do not enable this in production before the next eligible AdCP minor after
|
|
576
|
+
> November 20, 2026.** Until then the published protocol still describes
|
|
577
|
+
> superseded snapshots as preserved. The option is off by default, and
|
|
578
|
+
> retaining the whole chain, as the ledger does by default, stays conformant.
|
|
579
|
+
|
|
580
|
+
[adcontextprotocol/adcp#8100](https://github.com/adcontextprotocol/adcp/pull/8100)
|
|
581
|
+
separates the two things `status_retention_days` covers. **Metadata** for every
|
|
582
|
+
revision stays queryable for the whole window. **Content** (the rows) is owed
|
|
583
|
+
only for the revisions a buyer is likely to read, so a seller that publishes a
|
|
584
|
+
snapshot every 15 minutes need not keep thousands of full snapshots per period.
|
|
585
|
+
|
|
586
|
+
The window runs from the revision's `created_at`. Once a later revision names it
|
|
587
|
+
in `supersedes_reporting_revision_id`, it runs from that successor's
|
|
588
|
+
`created_at` instead. Within its window the seller must keep content readable
|
|
589
|
+
by exact read when the revision is:
|
|
590
|
+
|
|
591
|
+
- (a) the current, unsuperseded revision of its slice, which includes every
|
|
592
|
+
official revision;
|
|
593
|
+
- (b) the revision the current revision supersedes;
|
|
594
|
+
- (c) a superseded snapshot inside its recovery grace, `max(automated recovery
|
|
595
|
+
window, one hour)` after its successor's `created_at`;
|
|
596
|
+
- (d) named, for the requesting caller, by that caller's own reporting receipt
|
|
597
|
+
in any status, or by its current consumer status statement with status
|
|
598
|
+
`received`, `unreadable` or `content_mismatch`.
|
|
599
|
+
|
|
600
|
+
Any other superseded snapshot may be compacted to metadata only. An exact read
|
|
601
|
+
of a compacted revision returns the same nondisclosing `REFERENCE_NOT_FOUND` as
|
|
602
|
+
an unknown ID. A revision in (a) to (d) whose content cannot be served is
|
|
603
|
+
`SERVICE_UNAVAILABLE` (`REVISION_UNREADABLE`), never not-found.
|
|
604
|
+
|
|
605
|
+
```ts
|
|
606
|
+
// Core-only service
|
|
607
|
+
createReliableReportingService({
|
|
608
|
+
// ...
|
|
609
|
+
maintenance: { compaction: { enabled: true, limit: 100 } },
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// Production service
|
|
613
|
+
await createPostgresReliableReportingProductionService({
|
|
614
|
+
// ...
|
|
615
|
+
compaction: { enabled: true, limit: 100 },
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// Or call the store directly
|
|
619
|
+
await store.compactSupersededSnapshots({
|
|
620
|
+
statusRetentionDays: 30,
|
|
621
|
+
automatedRecoveryWindowSeconds: 3600,
|
|
622
|
+
limit: 100,
|
|
623
|
+
});
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
Both options take `{ enabled: true; limit?: number }` (default `limit` 100 per
|
|
627
|
+
pass) and reuse the service's `statusRetentionDays` and
|
|
628
|
+
`automatedRecoveryWindowSeconds`. The maintenance task is named
|
|
629
|
+
`snapshot compaction`.
|
|
630
|
+
|
|
631
|
+
A snapshot is compacted only when all of these hold:
|
|
632
|
+
|
|
633
|
+
- A later revision supersedes it, and it is not the revision the current
|
|
634
|
+
revision supersedes. Current and official revisions are never compacted.
|
|
635
|
+
- Its recovery grace has passed: `max(automatedRecoveryWindowSeconds, 3600)`
|
|
636
|
+
seconds since its successor's `created_at`.
|
|
637
|
+
- No caller is owed it. The check spans every caller: a receipt in any status
|
|
638
|
+
(current or superseded), or a current `received`, `unreadable` or
|
|
639
|
+
`content_mismatch` consumer status statement, from any caller keeps the
|
|
640
|
+
revision while its window (`statusRetentionDays` from its successor's
|
|
641
|
+
`created_at`) is open. After that window nothing is owed any more.
|
|
642
|
+
- The obligation holds no live worker lease, no Managed Delivery
|
|
643
|
+
materialization of that revision is still `pending`, and no live change-feed
|
|
644
|
+
consumer (updated within `maxFeedHoldDays`) has yet to pass that revision's
|
|
645
|
+
change row. This is the same hold retirement applies, narrowed to the
|
|
646
|
+
revision.
|
|
647
|
+
- It has rows. A revision with no rows has nothing to reclaim.
|
|
648
|
+
|
|
649
|
+
Compaction drops row content only. The revision header, its manifest and every
|
|
650
|
+
metadata listing stay, and `listRevisionMetadata` and the status views keep
|
|
651
|
+
listing it. `listRevisions`, which hydrates rows, omits compacted revisions.
|
|
652
|
+
The `revision` view of `get_reporting_status` for a compacted revision answers
|
|
653
|
+
as an unavailable lookup, like an exact read.
|
|
654
|
+
|
|
655
|
+
It uses the same three resumable phases as retirement. Under the account lock it
|
|
656
|
+
re-checks eligibility and marks the row set `pruning` (reads answer as expired;
|
|
657
|
+
no new revision can share its chunks). It then deletes external objects at their
|
|
658
|
+
recorded versions through the fenced delete path, skipping any object a row set
|
|
659
|
+
that is not being pruned still references. Finally it deletes the chunk
|
|
660
|
+
manifest and any PostgreSQL chunk body nothing else uses, and marks the row set
|
|
661
|
+
`pruned`. A crash after the first phase leaves a `pruning` row set that the next
|
|
662
|
+
pass finishes. Chunks shared by byte-identical revisions survive until the last
|
|
663
|
+
revision referencing them is compacted.
|
|
664
|
+
|
|
665
|
+
Compaction records no change-feed row: the revision's metadata did not change,
|
|
666
|
+
and a sink still catching up holds the revision back (above).
|
|
667
|
+
|
|
668
|
+
Compaction requires every object row binding's `keyTemplate` to include
|
|
669
|
+
`{revision_key}` (the default template does). Deletes now skip objects another
|
|
670
|
+
row set still references (see [Object storage](#object-storage-bindings)),
|
|
671
|
+
so this is defense in depth rather than the only guard: with `{revision_key}`
|
|
672
|
+
no two revisions ever name the same object, so compaction never depends on the
|
|
673
|
+
reference check. The SDK fails closed: `createReliableReportingService` and
|
|
674
|
+
`createPostgresReliableReportingProductionService` throw at construction when
|
|
675
|
+
compaction is enabled and a binding's template omits `{revision_key}`, and
|
|
676
|
+
`compactSupersededSnapshots` throws the same error before deleting anything.
|
|
677
|
+
|
|
549
678
|
### Core-only setup with row storage, change feed and retention
|
|
550
679
|
|
|
551
680
|
You do not need the production service (managed delivery, notifications and
|
|
@@ -611,6 +740,7 @@ Tasks, in order:
|
|
|
611
740
|
| `row upload sweep` | always | Resolves abandoned object uploads (100 per pass); no-op without row storage. |
|
|
612
741
|
| `change-feed pruning` | when `maintenance.changeFeed` is set | `pruneChanges` with `changeRetentionDays` and `maxFeedHoldDays`. |
|
|
613
742
|
| `retention` | when `maintenance.retention.enabled` is `true` | `retireExpiredPeriods` with the service's `statusRetentionDays` and the feed's `maxFeedHoldDays`. |
|
|
743
|
+
| `snapshot compaction` | when `maintenance.compaction.enabled` is `true` | `compactSupersededSnapshots` with the service's `statusRetentionDays` and `automatedRecoveryWindowSeconds` (see [Compacting superseded snapshots](#compacting-superseded-snapshots-opt-in)). |
|
|
614
744
|
|
|
615
745
|
Notes:
|
|
616
746
|
|
|
@@ -120,6 +120,15 @@ service runs for you. See
|
|
|
120
120
|
`status_retention_days` is the window you intend to keep. Retirement deletes
|
|
121
121
|
rows and ledger records for whole periods and cannot be undone. Keep backups
|
|
122
122
|
for any longer audit requirement.
|
|
123
|
+
7. **Snapshot compaction (optional).** Off by default; enable `compaction`
|
|
124
|
+
(`maintenance.compaction` on the Core-only service) only to reclaim the
|
|
125
|
+
content of superseded snapshots. Do not enable it in production before the
|
|
126
|
+
next eligible AdCP minor after November 20, 2026, when the protocol takes
|
|
127
|
+
compaction into effect. Roll it out after retention, with a small `limit`,
|
|
128
|
+
and watch the `snapshot compaction` task's `failed` IDs. See
|
|
129
|
+
[Compacting superseded snapshots](REPORTING-LEDGER.md#compacting-superseded-snapshots-opt-in)
|
|
130
|
+
for the rule and what it keeps. Compaction cannot be undone; backups hold
|
|
131
|
+
any longer evidence.
|
|
123
132
|
|
|
124
133
|
Alert on these row-storage signals:
|
|
125
134
|
|
|
@@ -127,6 +136,9 @@ Alert on these row-storage signals:
|
|
|
127
136
|
normal, and buyers see `SERVICE_UNAVAILABLE` for the affected revision.
|
|
128
137
|
- A growing count of `adcp_reporting_row_write_intents` in `sweeping` state,
|
|
129
138
|
or `sweepRowWriteIntents` reporting failures. Provider deletes are failing.
|
|
139
|
+
- The `snapshot compaction` task reporting `failed` revisions. Provider deletes
|
|
140
|
+
are failing; the revisions stay `pruning` (reads already answer not-found) and
|
|
141
|
+
the next pass resumes them.
|
|
130
142
|
- `retireExpiredPeriods` returning `failed` IDs. Usually a binding was removed
|
|
131
143
|
from configuration while revisions still reference it; keep retired bindings
|
|
132
144
|
configured.
|
package/docs/llms.txt
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Ad Context Protocol (AdCP)
|
|
2
2
|
|
|
3
|
-
> Generated at: 2026-10-
|
|
4
|
-
> Library: @adcp/sdk v15.
|
|
3
|
+
> Generated at: 2026-10-11
|
|
4
|
+
> Library: @adcp/sdk v15.8.0
|
|
5
5
|
> AdCP major version: 3
|
|
6
6
|
> Canonical URL: https://adcontextprotocol.github.io/adcp-client/llms.txt
|
|
7
7
|
> Note: the `Library` stamp reflects the package.json version at doc-generation time. The narrative below describes the surface that lands on the next-published minor — including any 6.7 helpers documented here ahead of the release tag.
|
|
@@ -2140,6 +2140,7 @@ These docs are available locally in the repo and hosted at https://adcontextprot
|
|
|
2140
2140
|
| Conformance (property-based fuzzing) | docs/guides/CONFORMANCE.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/CONFORMANCE.md) |
|
|
2141
2141
|
| Reporting source executor (seller adapters) | docs/guides/REPORTING-SOURCE-EXECUTOR.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/REPORTING-SOURCE-EXECUTOR.md) |
|
|
2142
2142
|
| Seller reporting ledger | docs/guides/REPORTING-LEDGER.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/REPORTING-LEDGER.md) |
|
|
2143
|
+
| Reporting ledger cutover from custom tables | docs/guides/REPORTING-LEDGER-CUTOVER.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/REPORTING-LEDGER-CUTOVER.md) |
|
|
2143
2144
|
| Buyer reporting reconciliation | docs/guides/REPORTING-RECONCILIATION.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/REPORTING-RECONCILIATION.md) |
|
|
2144
2145
|
| Reliable Reporting production operations | docs/guides/REPORTING-OPERATIONS.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/REPORTING-OPERATIONS.md) |
|
|
2145
2146
|
| Validate your agent (5-command checklist) | docs/guides/VALIDATE-YOUR-AGENT.md | [link](https://adcontextprotocol.github.io/adcp-client/guides/VALIDATE-YOUR-AGENT.md) |
|
|
@@ -11,8 +11,8 @@ below as the publication/deployment gate.
|
|
|
11
11
|
|
|
12
12
|
| Fact | Value |
|
|
13
13
|
| --- | --- |
|
|
14
|
-
| Exact npm package | `@adcp/sdk@15.
|
|
15
|
-
| npm integrity | registry-derived after publication; run `npm view @adcp/sdk@15.
|
|
14
|
+
| Exact npm package | `@adcp/sdk@15.8.0` |
|
|
15
|
+
| npm integrity | registry-derived after publication; run `npm view @adcp/sdk@15.8.0 dist.integrity` |
|
|
16
16
|
| Node.js runtime | `^20.19.0 || >=22.12.0` |
|
|
17
17
|
| Default AdCP wire release | `3.2.3` |
|
|
18
18
|
| Maintained wire releases | `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` |
|
|
@@ -21,8 +21,8 @@ below as the publication/deployment gate.
|
|
|
21
21
|
Install exact production inputs rather than a moving range or dist-tag:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npm install --save-exact '@adcp/sdk@15.
|
|
25
|
-
npm view '@adcp/sdk@15.
|
|
24
|
+
npm install --save-exact '@adcp/sdk@15.8.0'
|
|
25
|
+
npm view '@adcp/sdk@15.8.0' dist.integrity
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
### Required and optional peers
|