@adcp/sdk 14.0.0-rc.44 → 14.0.0-rc.46

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 (30) hide show
  1. package/dist/lib/discovery/validate-adagents.d.mts +2 -2
  2. package/dist/lib/discovery/validate-adagents.d.ts +2 -2
  3. package/dist/lib/discovery/validate-adagents.js +0 -3
  4. package/dist/lib/discovery/validate-adagents.mjs +0 -3
  5. package/dist/lib/protocols/transportDiagnostics.js +16 -9
  6. package/dist/lib/protocols/transportDiagnostics.mjs +16 -9
  7. package/dist/lib/registry/types.generated.d.mts +1 -1
  8. package/dist/lib/registry/types.generated.d.ts +1 -1
  9. package/dist/lib/reporting/reconciliation.js +42 -26
  10. package/dist/lib/reporting/reconciliation.mjs +42 -26
  11. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  12. package/dist/lib/testing/storyboard/compliance.js +2 -1
  13. package/dist/lib/testing/storyboard/compliance.mjs +2 -1
  14. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  15. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  16. package/dist/lib/v2/projection/creative-delivery.js +6 -2
  17. package/dist/lib/v2/projection/creative-delivery.mjs +12 -3
  18. package/dist/lib/v2/projection/v1-to-v2.d.mts +7 -0
  19. package/dist/lib/v2/projection/v1-to-v2.d.ts +7 -0
  20. package/dist/lib/v2/projection/v1-to-v2.js +2 -0
  21. package/dist/lib/v2/projection/v1-to-v2.mjs +1 -0
  22. package/dist/lib/version.d.mts +3 -3
  23. package/dist/lib/version.d.ts +3 -3
  24. package/dist/lib/version.js +3 -3
  25. package/dist/lib/version.mjs +3 -3
  26. package/docs/TYPE-SUMMARY.md +1 -1
  27. package/docs/guides/REPORTING-RECONCILIATION.md +2 -0
  28. package/docs/llms.txt +1 -1
  29. package/docs/migration-14.x-rc-worksheet.md +4 -4
  30. package/package.json +1 -1
@@ -82,8 +82,8 @@ export interface ValidateAdAgentsOptions {
82
82
  * is used for every direct, authoritative-location, ads.txt, and manager
83
83
  * request, so `AbortSignal.timeout(...)` is an absolute operation deadline
84
84
  * rather than a fresh budget per hop. `timeoutMs` remains the ceiling for
85
- * each individual request. Once aborted, discovery throws the abort reason
86
- * and does not start a later fallback request.
85
+ * each individual request. Once aborted, discovery returns an invalid
86
+ * structured result and does not start a later fallback request.
87
87
  */
88
88
  signal?: AbortSignal;
89
89
  /** Maximum response body bytes for adagents.json and ads.txt fetches (default 256 KiB). */
@@ -82,8 +82,8 @@ export interface ValidateAdAgentsOptions {
82
82
  * is used for every direct, authoritative-location, ads.txt, and manager
83
83
  * request, so `AbortSignal.timeout(...)` is an absolute operation deadline
84
84
  * rather than a fresh budget per hop. `timeoutMs` remains the ceiling for
85
- * each individual request. Once aborted, discovery throws the abort reason
86
- * and does not start a later fallback request.
85
+ * each individual request. Once aborted, discovery returns an invalid
86
+ * structured result and does not start a later fallback request.
87
87
  */
88
88
  signal?: AbortSignal;
89
89
  /** Maximum response body bytes for adagents.json and ads.txt fetches (default 256 KiB). */
@@ -304,9 +304,6 @@ async function rawFetch(url, opts) {
304
304
  const text = typeof decoded === "string" ? decoded : JSON.stringify(decoded);
305
305
  return { kind: "ok-text", text, url: result.url };
306
306
  } catch (err) {
307
- if (opts.signal?.aborted || err instanceof Error && err.name === "AbortError") {
308
- throw err;
309
- }
310
307
  if (err instanceof import_adagents_redirects.AdAgentsRedirectRefusedError) {
311
308
  return { kind: "redirect_refused", message: err.message };
312
309
  }
@@ -280,9 +280,6 @@ async function rawFetch(url, opts) {
280
280
  const text = typeof decoded === "string" ? decoded : JSON.stringify(decoded);
281
281
  return { kind: "ok-text", text, url: result.url };
282
282
  } catch (err) {
283
- if (opts.signal?.aborted || err instanceof Error && err.name === "AbortError") {
284
- throw err;
285
- }
286
283
  if (err instanceof AdAgentsRedirectRefusedError) {
287
284
  return { kind: "redirect_refused", message: err.message };
288
285
  }
@@ -147,10 +147,13 @@ function wrapFetchWithTransportDiagnostics(upstream) {
147
147
  };
148
148
  const responseBody = responseBodySnippet(response);
149
149
  if (!responseBody) {
150
- emitTransportActivity(handler, {
151
- ...responseEvent,
152
- responseBodyTruncated: true
153
- });
150
+ emitTransportActivity(
151
+ handler,
152
+ response.body ? {
153
+ ...responseEvent,
154
+ responseBodyTruncated: true
155
+ } : responseEvent
156
+ );
154
157
  } else {
155
158
  void responseBody.then(
156
159
  (captured) => {
@@ -219,12 +222,16 @@ function sanitizeResponseHeaders(headers) {
219
222
  }
220
223
  function headerEntries(headers) {
221
224
  if (!headers) return [];
222
- if (headers instanceof Headers) {
225
+ if (Array.isArray(headers)) return headers.map(([key, value]) => [key, String(value)]);
226
+ const forEach = headers.forEach;
227
+ if (typeof forEach === "function") {
223
228
  const entries = [];
224
- headers.forEach((value, key) => entries.push([key, value]));
225
- return entries;
229
+ try {
230
+ forEach.call(headers, (value, key) => entries.push([String(key), String(value)]));
231
+ return entries;
232
+ } catch {
233
+ }
226
234
  }
227
- if (Array.isArray(headers)) return headers.map(([key, value]) => [key, value]);
228
235
  return Object.entries(headers).map(([key, value]) => [key, String(value)]);
229
236
  }
230
237
  function isSafeCorrelationHeader(lower) {
@@ -251,7 +258,7 @@ function responseBodySnippet(response) {
251
258
  const contentType = response.headers.get("content-type") ?? "";
252
259
  if (!isDiagnosticTextContentType(contentType)) return void 0;
253
260
  const declaredLength = parseDiagnosticContentLength(response.headers.get("content-length"));
254
- if (declaredLength !== void 0 && declaredLength > BODY_SNIPPET_LIMIT) return void 0;
261
+ if (declaredLength === void 0 || declaredLength > BODY_SNIPPET_LIMIT) return void 0;
255
262
  let diagnosticResponse;
256
263
  try {
257
264
  diagnosticResponse = response.clone();
@@ -123,10 +123,13 @@ function wrapFetchWithTransportDiagnostics(upstream) {
123
123
  };
124
124
  const responseBody = responseBodySnippet(response);
125
125
  if (!responseBody) {
126
- emitTransportActivity(handler, {
127
- ...responseEvent,
128
- responseBodyTruncated: true
129
- });
126
+ emitTransportActivity(
127
+ handler,
128
+ response.body ? {
129
+ ...responseEvent,
130
+ responseBodyTruncated: true
131
+ } : responseEvent
132
+ );
130
133
  } else {
131
134
  void responseBody.then(
132
135
  (captured) => {
@@ -195,12 +198,16 @@ function sanitizeResponseHeaders(headers) {
195
198
  }
196
199
  function headerEntries(headers) {
197
200
  if (!headers) return [];
198
- if (headers instanceof Headers) {
201
+ if (Array.isArray(headers)) return headers.map(([key, value]) => [key, String(value)]);
202
+ const forEach = headers.forEach;
203
+ if (typeof forEach === "function") {
199
204
  const entries = [];
200
- headers.forEach((value, key) => entries.push([key, value]));
201
- return entries;
205
+ try {
206
+ forEach.call(headers, (value, key) => entries.push([String(key), String(value)]));
207
+ return entries;
208
+ } catch {
209
+ }
202
210
  }
203
- if (Array.isArray(headers)) return headers.map(([key, value]) => [key, value]);
204
211
  return Object.entries(headers).map(([key, value]) => [key, String(value)]);
205
212
  }
206
213
  function isSafeCorrelationHeader(lower) {
@@ -227,7 +234,7 @@ function responseBodySnippet(response) {
227
234
  const contentType = response.headers.get("content-type") ?? "";
228
235
  if (!isDiagnosticTextContentType(contentType)) return void 0;
229
236
  const declaredLength = parseDiagnosticContentLength(response.headers.get("content-length"));
230
- if (declaredLength !== void 0 && declaredLength > BODY_SNIPPET_LIMIT) return void 0;
237
+ if (declaredLength === void 0 || declaredLength > BODY_SNIPPET_LIMIT) return void 0;
231
238
  let diagnosticResponse;
232
239
  try {
233
240
  diagnosticResponse = response.clone();
@@ -4316,7 +4316,7 @@ export interface components {
4316
4316
  adcp_version: string;
4317
4317
  verified_at: string;
4318
4318
  /** @description Specialisms demonstrably passed (enums/specialism.json). Preview specialisms are excluded from stable badges. */
4319
- verified_specialisms: ("audience-sync" | "brand-rights" | "buyer-activation" | "buyer-discovery" | "buyer-monitoring" | "buyer-negotiation" | "buyer-recovery" | "collection-lists" | "content-standards" | "creative-ad-server" | "creative-generative" | "creative-template" | "creative-transformers" | "governance-aware-seller" | "governance-delivery-monitor" | "governance-spend-authority" | "property-lists" | "sales-broadcast-tv" | "sales-catalog-driven" | "sales-dooh" | "sales-guaranteed" | "sales-non-guaranteed" | "sales-proposal-mode" | "sales-social" | "signal-marketplace" | "orchestrator-multi-agent" | "signal-owned" | "signed-requests" | "sponsored-intelligence")[];
4319
+ verified_specialisms: ("audience-sync" | "brand-rights" | "buyer-activation" | "buyer-discovery" | "buyer-monitoring" | "buyer-negotiation" | "buyer-recovery" | "collection-lists" | "content-standards" | "creative-ad-server" | "creative-generative" | "creative-template" | "creative-transformers" | "governance-aware-seller" | "governance-delivery-monitor" | "governance-spend-authority" | "property-lists" | "sales-broadcast-tv" | "sales-catalog-driven" | "sales-dooh" | "sales-exchange" | "sales-guaranteed" | "sales-non-guaranteed" | "sales-proposal-mode" | "sales-retail-media" | "sales-streaming-tv" | "sales-social" | "signal-marketplace" | "orchestrator-multi-agent" | "signal-owned" | "signed-requests" | "sponsored-intelligence")[];
4320
4320
  /** @description Evidence modes earned. 'spec' = AdCP storyboards were exercised; 'live' = production traffic was observed. Independent of grading_profile and always non-empty when a badge is present. */
4321
4321
  verification_modes: ("spec" | "live")[];
4322
4322
  /**
@@ -4316,7 +4316,7 @@ export interface components {
4316
4316
  adcp_version: string;
4317
4317
  verified_at: string;
4318
4318
  /** @description Specialisms demonstrably passed (enums/specialism.json). Preview specialisms are excluded from stable badges. */
4319
- verified_specialisms: ("audience-sync" | "brand-rights" | "buyer-activation" | "buyer-discovery" | "buyer-monitoring" | "buyer-negotiation" | "buyer-recovery" | "collection-lists" | "content-standards" | "creative-ad-server" | "creative-generative" | "creative-template" | "creative-transformers" | "governance-aware-seller" | "governance-delivery-monitor" | "governance-spend-authority" | "property-lists" | "sales-broadcast-tv" | "sales-catalog-driven" | "sales-dooh" | "sales-guaranteed" | "sales-non-guaranteed" | "sales-proposal-mode" | "sales-social" | "signal-marketplace" | "orchestrator-multi-agent" | "signal-owned" | "signed-requests" | "sponsored-intelligence")[];
4319
+ verified_specialisms: ("audience-sync" | "brand-rights" | "buyer-activation" | "buyer-discovery" | "buyer-monitoring" | "buyer-negotiation" | "buyer-recovery" | "collection-lists" | "content-standards" | "creative-ad-server" | "creative-generative" | "creative-template" | "creative-transformers" | "governance-aware-seller" | "governance-delivery-monitor" | "governance-spend-authority" | "property-lists" | "sales-broadcast-tv" | "sales-catalog-driven" | "sales-dooh" | "sales-exchange" | "sales-guaranteed" | "sales-non-guaranteed" | "sales-proposal-mode" | "sales-retail-media" | "sales-streaming-tv" | "sales-social" | "signal-marketplace" | "orchestrator-multi-agent" | "signal-owned" | "signed-requests" | "sponsored-intelligence")[];
4320
4320
  /** @description Evidence modes earned. 'spec' = AdCP storyboards were exercised; 'live' = production traffic was observed. Independent of grading_profile and always non-empty when a badge is present. */
4321
4321
  verification_modes: ("spec" | "live")[];
4322
4322
  /**
@@ -419,22 +419,16 @@ function assertReportingLedgerGraph(accountId, obligations, revisions, materiali
419
419
  }
420
420
  referencedRevisions.add(materialization.reporting_revision_id);
421
421
  }
422
- const materializedObligations = new Set(
423
- [...materializations.values()].map((materialization) => materialization.reporting_obligation_id)
424
- );
425
- const directCoreScopeCounts = /* @__PURE__ */ new Map();
422
+ const obligationScopes = /* @__PURE__ */ new Set();
426
423
  for (const obligation of obligations.values()) {
427
- if (!isDirectCoreObligation(obligation) || !Array.isArray(obligation.media_buy_ids) || materializedObligations.has(obligation.reporting_obligation_id)) {
428
- continue;
424
+ if (Array.isArray(obligation.media_buy_ids)) {
425
+ obligationScopes.add(reportingRevisionScopeKey(obligation));
429
426
  }
430
- const scopeKey = reportingRevisionScopeKey(obligation);
431
- directCoreScopeCounts.set(scopeKey, (directCoreScopeCounts.get(scopeKey) ?? 0) + 1);
432
427
  }
433
428
  for (const revision of revisions.values()) {
434
429
  const referenced = referencedRevisions.has(revision.reporting_revision_id);
435
- const directCoreScopeCount = Array.isArray(revision.media_buy_ids) ? directCoreScopeCounts.get(reportingRevisionScopeKey(revision)) ?? 0 : 0;
436
- const directlyScopedCoreRevision = directCoreScopeCount > 0;
437
- if (revision.account_id !== accountId || !referenced && !directlyScopedCoreRevision || !(0, import_evidence.isReportingControlTotals)(revision.control_totals)) {
430
+ const scoped = Array.isArray(revision.media_buy_ids) && obligationScopes.has(reportingRevisionScopeKey(revision));
431
+ if (revision.account_id !== accountId || !referenced && !scoped || !(0, import_evidence.isReportingControlTotals)(revision.control_totals)) {
438
432
  fail();
439
433
  }
440
434
  }
@@ -447,9 +441,6 @@ function assertReportingLedgerGraph(accountId, obligations, revisions, materiali
447
441
  }
448
442
  }
449
443
  }
450
- function isDirectCoreObligation(obligation) {
451
- return obligation.reconciliation_mode === "delivery_only" && obligation.destination_ref === void 0;
452
- }
453
444
  function revisionMatchesObligationScope(revision, obligation) {
454
445
  return 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) && sameStringSet(revision.media_buy_ids, obligation.media_buy_ids) && same(revision.period, obligation.period);
455
446
  }
@@ -481,10 +472,9 @@ function selectCurrent(obligation, ledger, expected) {
481
472
  (item) => item.reporting_obligation_id === obligation.reporting_obligation_id
482
473
  );
483
474
  const revisionIds = new Set(attempts.map((item) => item.reporting_revision_id));
484
- const candidates = ledger.revisions.filter((item) => {
485
- if (attempts.length > 0) return revisionIds.has(item.reporting_revision_id);
486
- return isDirectCoreObligation(obligation) && revisionMatchesObligationScope(item, obligation);
487
- });
475
+ const candidates = ledger.revisions.filter(
476
+ (item) => revisionIds.has(item.reporting_revision_id) || revisionMatchesObligationScope(item, obligation)
477
+ );
488
478
  const receipts = ledger.receipts.filter((item) => item.reporting_obligation_id === obligation.reporting_obligation_id);
489
479
  const successfulAttempts = attempts.filter((item) => item.status === "available" || item.status === "delivered");
490
480
  const acceptedReceipts = receipts.filter((item) => item.status === "accepted");
@@ -499,12 +489,33 @@ function selectCurrent(obligation, ledger, expected) {
499
489
  ) || countMismatch(obligation.receipt_count, receipts.length, receiptCountsRequired) || countMismatch(obligation.accepted_receipt_count, acceptedReceipts.length, receiptCountsRequired)) {
500
490
  reasons.push("ASSOCIATED_HISTORY_INCOMPLETE");
501
491
  }
502
- const superseded = new Set(
503
- candidates.map((item) => item.supersedes_reporting_revision_id).filter((id) => Boolean(id))
504
- );
505
- const candidateIds = new Set(candidates.map((item) => item.reporting_revision_id));
492
+ const candidateById = new Map(candidates.map((item) => [item.reporting_revision_id, item]));
493
+ const superseded = /* @__PURE__ */ new Set();
494
+ let invalidTopology = false;
495
+ for (const candidate of candidates) {
496
+ const predecessorId = candidate.supersedes_reporting_revision_id;
497
+ if (!predecessorId) continue;
498
+ if (superseded.has(predecessorId) || candidateById.get(predecessorId)?.finality === "official") {
499
+ invalidTopology = true;
500
+ }
501
+ superseded.add(predecessorId);
502
+ }
503
+ const checked = /* @__PURE__ */ new Set();
504
+ for (const candidate of candidates) {
505
+ const path = /* @__PURE__ */ new Set();
506
+ let cursor = candidate;
507
+ while (cursor && !checked.has(cursor.reporting_revision_id)) {
508
+ if (path.has(cursor.reporting_revision_id)) {
509
+ invalidTopology = true;
510
+ break;
511
+ }
512
+ path.add(cursor.reporting_revision_id);
513
+ cursor = candidateById.get(cursor.supersedes_reporting_revision_id ?? "");
514
+ }
515
+ for (const id of path) checked.add(id);
516
+ }
506
517
  if (candidates.some(
507
- (item) => item.supersedes_reporting_revision_id && !candidateIds.has(item.supersedes_reporting_revision_id)
518
+ (item) => item.supersedes_reporting_revision_id && !candidateById.has(item.supersedes_reporting_revision_id)
508
519
  )) {
509
520
  reasons.push("REVISION_PREDECESSOR_MISSING");
510
521
  }
@@ -514,11 +525,16 @@ function selectCurrent(obligation, ledger, expected) {
514
525
  reasons.push("REVISION_CHAIN_SCOPE_MISMATCH");
515
526
  }
516
527
  const current = candidates.filter((item) => !superseded.has(item.reporting_revision_id));
517
- if (current.length !== 1) {
518
- reasons.push(current.length === 0 ? "MISSING_CURRENT_REVISION" : "AMBIGUOUS_REVISION_CHAIN");
528
+ const officials = candidates.filter((item) => item.finality === "official");
529
+ if (invalidTopology || officials.length > 1 || current.length > 2 || current.length === 2 && officials.length !== 1) {
530
+ reasons.push("AMBIGUOUS_REVISION_CHAIN");
531
+ return { reasons };
532
+ }
533
+ const revision = officials[0] ?? current[0];
534
+ if (!revision) {
535
+ reasons.push("MISSING_CURRENT_REVISION");
519
536
  return { reasons };
520
537
  }
521
- const revision = current[0];
522
538
  const revisionControlTotalsValid = (0, import_evidence.isReportingControlTotals)(revision.control_totals);
523
539
  if (!revisionControlTotalsValid) reasons.push("REVISION_CONTROL_TOTALS_INVALID");
524
540
  if (!revision.report_definition_uri || !revision.report_definition_sha256) {
@@ -396,22 +396,16 @@ function assertReportingLedgerGraph(accountId, obligations, revisions, materiali
396
396
  }
397
397
  referencedRevisions.add(materialization.reporting_revision_id);
398
398
  }
399
- const materializedObligations = new Set(
400
- [...materializations.values()].map((materialization) => materialization.reporting_obligation_id)
401
- );
402
- const directCoreScopeCounts = /* @__PURE__ */ new Map();
399
+ const obligationScopes = /* @__PURE__ */ new Set();
403
400
  for (const obligation of obligations.values()) {
404
- if (!isDirectCoreObligation(obligation) || !Array.isArray(obligation.media_buy_ids) || materializedObligations.has(obligation.reporting_obligation_id)) {
405
- continue;
401
+ if (Array.isArray(obligation.media_buy_ids)) {
402
+ obligationScopes.add(reportingRevisionScopeKey(obligation));
406
403
  }
407
- const scopeKey = reportingRevisionScopeKey(obligation);
408
- directCoreScopeCounts.set(scopeKey, (directCoreScopeCounts.get(scopeKey) ?? 0) + 1);
409
404
  }
410
405
  for (const revision of revisions.values()) {
411
406
  const referenced = referencedRevisions.has(revision.reporting_revision_id);
412
- const directCoreScopeCount = Array.isArray(revision.media_buy_ids) ? directCoreScopeCounts.get(reportingRevisionScopeKey(revision)) ?? 0 : 0;
413
- const directlyScopedCoreRevision = directCoreScopeCount > 0;
414
- if (revision.account_id !== accountId || !referenced && !directlyScopedCoreRevision || !isReportingControlTotals(revision.control_totals)) {
407
+ const scoped = Array.isArray(revision.media_buy_ids) && obligationScopes.has(reportingRevisionScopeKey(revision));
408
+ if (revision.account_id !== accountId || !referenced && !scoped || !isReportingControlTotals(revision.control_totals)) {
415
409
  fail();
416
410
  }
417
411
  }
@@ -424,9 +418,6 @@ function assertReportingLedgerGraph(accountId, obligations, revisions, materiali
424
418
  }
425
419
  }
426
420
  }
427
- function isDirectCoreObligation(obligation) {
428
- return obligation.reconciliation_mode === "delivery_only" && obligation.destination_ref === void 0;
429
- }
430
421
  function revisionMatchesObligationScope(revision, obligation) {
431
422
  return 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) && sameStringSet(revision.media_buy_ids, obligation.media_buy_ids) && same(revision.period, obligation.period);
432
423
  }
@@ -458,10 +449,9 @@ function selectCurrent(obligation, ledger, expected) {
458
449
  (item) => item.reporting_obligation_id === obligation.reporting_obligation_id
459
450
  );
460
451
  const revisionIds = new Set(attempts.map((item) => item.reporting_revision_id));
461
- const candidates = ledger.revisions.filter((item) => {
462
- if (attempts.length > 0) return revisionIds.has(item.reporting_revision_id);
463
- return isDirectCoreObligation(obligation) && revisionMatchesObligationScope(item, obligation);
464
- });
452
+ const candidates = ledger.revisions.filter(
453
+ (item) => revisionIds.has(item.reporting_revision_id) || revisionMatchesObligationScope(item, obligation)
454
+ );
465
455
  const receipts = ledger.receipts.filter((item) => item.reporting_obligation_id === obligation.reporting_obligation_id);
466
456
  const successfulAttempts = attempts.filter((item) => item.status === "available" || item.status === "delivered");
467
457
  const acceptedReceipts = receipts.filter((item) => item.status === "accepted");
@@ -476,12 +466,33 @@ function selectCurrent(obligation, ledger, expected) {
476
466
  ) || countMismatch(obligation.receipt_count, receipts.length, receiptCountsRequired) || countMismatch(obligation.accepted_receipt_count, acceptedReceipts.length, receiptCountsRequired)) {
477
467
  reasons.push("ASSOCIATED_HISTORY_INCOMPLETE");
478
468
  }
479
- const superseded = new Set(
480
- candidates.map((item) => item.supersedes_reporting_revision_id).filter((id) => Boolean(id))
481
- );
482
- const candidateIds = new Set(candidates.map((item) => item.reporting_revision_id));
469
+ const candidateById = new Map(candidates.map((item) => [item.reporting_revision_id, item]));
470
+ const superseded = /* @__PURE__ */ new Set();
471
+ let invalidTopology = false;
472
+ for (const candidate of candidates) {
473
+ const predecessorId = candidate.supersedes_reporting_revision_id;
474
+ if (!predecessorId) continue;
475
+ if (superseded.has(predecessorId) || candidateById.get(predecessorId)?.finality === "official") {
476
+ invalidTopology = true;
477
+ }
478
+ superseded.add(predecessorId);
479
+ }
480
+ const checked = /* @__PURE__ */ new Set();
481
+ for (const candidate of candidates) {
482
+ const path = /* @__PURE__ */ new Set();
483
+ let cursor = candidate;
484
+ while (cursor && !checked.has(cursor.reporting_revision_id)) {
485
+ if (path.has(cursor.reporting_revision_id)) {
486
+ invalidTopology = true;
487
+ break;
488
+ }
489
+ path.add(cursor.reporting_revision_id);
490
+ cursor = candidateById.get(cursor.supersedes_reporting_revision_id ?? "");
491
+ }
492
+ for (const id of path) checked.add(id);
493
+ }
483
494
  if (candidates.some(
484
- (item) => item.supersedes_reporting_revision_id && !candidateIds.has(item.supersedes_reporting_revision_id)
495
+ (item) => item.supersedes_reporting_revision_id && !candidateById.has(item.supersedes_reporting_revision_id)
485
496
  )) {
486
497
  reasons.push("REVISION_PREDECESSOR_MISSING");
487
498
  }
@@ -491,11 +502,16 @@ function selectCurrent(obligation, ledger, expected) {
491
502
  reasons.push("REVISION_CHAIN_SCOPE_MISMATCH");
492
503
  }
493
504
  const current = candidates.filter((item) => !superseded.has(item.reporting_revision_id));
494
- if (current.length !== 1) {
495
- reasons.push(current.length === 0 ? "MISSING_CURRENT_REVISION" : "AMBIGUOUS_REVISION_CHAIN");
505
+ const officials = candidates.filter((item) => item.finality === "official");
506
+ if (invalidTopology || officials.length > 1 || current.length > 2 || current.length === 2 && officials.length !== 1) {
507
+ reasons.push("AMBIGUOUS_REVISION_CHAIN");
508
+ return { reasons };
509
+ }
510
+ const revision = officials[0] ?? current[0];
511
+ if (!revision) {
512
+ reasons.push("MISSING_CURRENT_REVISION");
496
513
  return { reasons };
497
514
  }
498
- const revision = current[0];
499
515
  const revisionControlTotalsValid = isReportingControlTotals(revision.control_totals);
500
516
  if (!revisionControlTotalsValid) reasons.push("REVISION_CONTROL_TOTALS_INVALID");
501
517
  if (!revision.report_definition_uri || !revision.report_definition_sha256) {
@@ -4,5 +4,5 @@
4
4
  "source_sha": "4e553ad955f83b49c7d221ab5c3ff78237ad02e3",
5
5
  "source_tarball_sha256": "580656d6466ef9f0d1119985e6726c2efea718dc671e2ad30957fcb2fd54af0f",
6
6
  "upstream_adcp_version": "2.5.3",
7
- "synced_at": "2026-09-22T10:35:10.302Z"
7
+ "synced_at": "2026-09-22T19:09:10.936Z"
8
8
  }
@@ -448,8 +448,9 @@ function loadSpecialismDetail(slug, options = {}) {
448
448
  }
449
449
  const required = [];
450
450
  const unresolved = [];
451
+ const storyboardsById = new Map(listAllComplianceStoryboards(options).map((sb) => [sb.id, sb]));
451
452
  for (const ref of storyboard.requires_scenarios ?? []) {
452
- const sb = getComplianceStoryboardById(ref, options);
453
+ const sb = storyboardsById.get(ref);
453
454
  if (sb) required.push(sb);
454
455
  else unresolved.push(ref);
455
456
  }
@@ -413,8 +413,9 @@ function loadSpecialismDetail(slug, options = {}) {
413
413
  }
414
414
  const required = [];
415
415
  const unresolved = [];
416
+ const storyboardsById = new Map(listAllComplianceStoryboards(options).map((sb) => [sb.id, sb]));
416
417
  for (const ref of storyboard.requires_scenarios ?? []) {
417
- const sb = getComplianceStoryboardById(ref, options);
418
+ const sb = storyboardsById.get(ref);
418
419
  if (sb) required.push(sb);
419
420
  else unresolved.push(ref);
420
421
  }
@@ -197,7 +197,7 @@ export interface UpstreamTrafficQueryResult {
197
197
  */
198
198
  export declare function runValidations(validations: StoryboardValidation[], context: ValidationContext): ValidationResult[];
199
199
  /** Capability semver emitted in run summaries and used for advisory expiry. */
200
- export declare const RUNNER_CAPABILITY_VERSION = "14.0.0-rc.44";
200
+ export declare const RUNNER_CAPABILITY_VERSION = "14.0.0-rc.46";
201
201
  /** True when a failed validation contributes to the owning step's grade. */
202
202
  export declare function validationFailsStep(result: ValidationResult): boolean;
203
203
  /**
@@ -197,7 +197,7 @@ export interface UpstreamTrafficQueryResult {
197
197
  */
198
198
  export declare function runValidations(validations: StoryboardValidation[], context: ValidationContext): ValidationResult[];
199
199
  /** Capability semver emitted in run summaries and used for advisory expiry. */
200
- export declare const RUNNER_CAPABILITY_VERSION = "14.0.0-rc.44";
200
+ export declare const RUNNER_CAPABILITY_VERSION = "14.0.0-rc.46";
201
201
  /** True when a failed validation contributes to the owning step's grade. */
202
202
  export declare function validationFailsStep(result: ValidationResult): boolean;
203
203
  /**
@@ -537,7 +537,7 @@ function selectLegacyRef(creative, container, canonicalFormatLegacyResolver, ope
537
537
  return void 0;
538
538
  }
539
539
  candidates = candidates.filter(
540
- (candidate) => candidate.formatOptionRef && sameOptionReference(candidate.formatOptionRef, creativeOptionRef)
540
+ (candidate) => candidate.formatOptionRef ? sameOptionReference(candidate.formatOptionRef, creativeOptionRef) : (0, import_v1_to_v2.migratedFormatOptionId)(candidate.ref) === creativeOptionRef.format_option_id
541
541
  );
542
542
  } else if (selectedOptionRefs.length > 0) {
543
543
  candidates = candidates.filter(
@@ -850,6 +850,10 @@ function projectCreativeForDelivery(creative, selectorContainer, wireMode = "can
850
850
  );
851
851
  }
852
852
  if (resolved) return projectCreative(creativeRecord, resolved[0]);
853
+ const pinned = optionReference(creativeRecord.format_option_ref) !== void 0;
854
+ if (candidates.length === 1 && !pinned) {
855
+ return projectCreative(creativeRecord, candidates[0].ref);
856
+ }
853
857
  if (candidates.length > 1) {
854
858
  throw new CreativeFormatProjectionError(
855
859
  operation,
@@ -857,7 +861,7 @@ function projectCreativeForDelivery(creative, selectorContainer, wireMode = "can
857
861
  `the seller advertised ${candidates.length} legacy refs for canonical kind ${creativeRecord.format_kind}`
858
862
  );
859
863
  }
860
- const reason = "the selected seller product did not provide one unambiguous legacy format reference";
864
+ const reason = pinned ? "the creative is pinned to a format option that names none of the selected seller product legacy format references" : "the selected seller product did not provide one unambiguous legacy format reference";
861
865
  throw new CreativeFormatProjectionError(operation, creativeId, reason);
862
866
  }
863
867
  return creative;
@@ -1,4 +1,9 @@
1
- import { canonicalDeclarationFromBareId, projectV1ProductToV2, resolveCanonicalFormatKind } from "./v1-to-v2.mjs";
1
+ import {
2
+ canonicalDeclarationFromBareId,
3
+ migratedFormatOptionId,
4
+ projectV1ProductToV2,
5
+ resolveCanonicalFormatKind
6
+ } from "./v1-to-v2.mjs";
2
7
  import { CanonicalFormatLegacyResolutionError, resolveCanonicalFormatLegacyRefs } from "./v2-to-v1.mjs";
3
8
  import {
4
9
  legacyFormatRefsForDeclaration,
@@ -512,7 +517,7 @@ function selectLegacyRef(creative, container, canonicalFormatLegacyResolver, ope
512
517
  return void 0;
513
518
  }
514
519
  candidates = candidates.filter(
515
- (candidate) => candidate.formatOptionRef && sameOptionReference(candidate.formatOptionRef, creativeOptionRef)
520
+ (candidate) => candidate.formatOptionRef ? sameOptionReference(candidate.formatOptionRef, creativeOptionRef) : migratedFormatOptionId(candidate.ref) === creativeOptionRef.format_option_id
516
521
  );
517
522
  } else if (selectedOptionRefs.length > 0) {
518
523
  candidates = candidates.filter(
@@ -825,6 +830,10 @@ function projectCreativeForDelivery(creative, selectorContainer, wireMode = "can
825
830
  );
826
831
  }
827
832
  if (resolved) return projectCreative(creativeRecord, resolved[0]);
833
+ const pinned = optionReference(creativeRecord.format_option_ref) !== void 0;
834
+ if (candidates.length === 1 && !pinned) {
835
+ return projectCreative(creativeRecord, candidates[0].ref);
836
+ }
828
837
  if (candidates.length > 1) {
829
838
  throw new CreativeFormatProjectionError(
830
839
  operation,
@@ -832,7 +841,7 @@ function projectCreativeForDelivery(creative, selectorContainer, wireMode = "can
832
841
  `the seller advertised ${candidates.length} legacy refs for canonical kind ${creativeRecord.format_kind}`
833
842
  );
834
843
  }
835
- const reason = "the selected seller product did not provide one unambiguous legacy format reference";
844
+ const reason = pinned ? "the creative is pinned to a format option that names none of the selected seller product legacy format references" : "the selected seller product did not provide one unambiguous legacy format reference";
836
845
  throw new CreativeFormatProjectionError(operation, creativeId, reason);
837
846
  }
838
847
  return creative;
@@ -48,6 +48,13 @@
48
48
  import type { V1ProductInput, V1FormatId, V2Product, V2ProductFormatDeclaration, ProjectionDiagnostic, CanonicalFormatKind } from './types.mjs';
49
49
  import { type ProjectionCatalogSnapshot } from './catalog-snapshot.mjs';
50
50
  import type { CanonicalFormatLegacyRoute } from './legacy-routes.mjs';
51
+ /**
52
+ * Give an unnamed projected option a stable, opaque identity derived from the
53
+ * complete legacy tuple. Positional IDs are unsafe: a seller reordering its
54
+ * `format_ids` array could otherwise make a persisted canonical selection
55
+ * resolve to a different legacy format on the next discovery refresh.
56
+ */
57
+ export declare function migratedFormatOptionId(fid: V1FormatId): string;
51
58
  export interface V1ToV2Result {
52
59
  v2: V2Product;
53
60
  diagnostics: ProjectionDiagnostic[];
@@ -48,6 +48,13 @@
48
48
  import type { V1ProductInput, V1FormatId, V2Product, V2ProductFormatDeclaration, ProjectionDiagnostic, CanonicalFormatKind } from './types';
49
49
  import { type ProjectionCatalogSnapshot } from './catalog-snapshot';
50
50
  import type { CanonicalFormatLegacyRoute } from './legacy-routes';
51
+ /**
52
+ * Give an unnamed projected option a stable, opaque identity derived from the
53
+ * complete legacy tuple. Positional IDs are unsafe: a seller reordering its
54
+ * `format_ids` array could otherwise make a persisted canonical selection
55
+ * resolve to a different legacy format on the next discovery refresh.
56
+ */
57
+ export declare function migratedFormatOptionId(fid: V1FormatId): string;
51
58
  export interface V1ToV2Result {
52
59
  v2: V2Product;
53
60
  diagnostics: ProjectionDiagnostic[];
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  var v1_to_v2_exports = {};
20
20
  __export(v1_to_v2_exports, {
21
21
  canonicalDeclarationFromBareId: () => canonicalDeclarationFromBareId,
22
+ migratedFormatOptionId: () => migratedFormatOptionId,
22
23
  projectV1ProductToV2: () => projectV1ProductToV2,
23
24
  resolveCanonicalFormatKind: () => resolveCanonicalFormatKind
24
25
  });
@@ -369,6 +370,7 @@ function resolveCanonicalFormatKind(id, options) {
369
370
  // Annotate the CommonJS export names for ESM import in node:
370
371
  0 && (module.exports = {
371
372
  canonicalDeclarationFromBareId,
373
+ migratedFormatOptionId,
372
374
  projectV1ProductToV2,
373
375
  resolveCanonicalFormatKind
374
376
  });
@@ -348,6 +348,7 @@ function resolveCanonicalFormatKind(id, options) {
348
348
  }
349
349
  export {
350
350
  canonicalDeclarationFromBareId,
351
+ migratedFormatOptionId,
351
352
  projectV1ProductToV2,
352
353
  resolveCanonicalFormatKind
353
354
  };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * AdCP SDK library version
3
3
  */
4
- export declare const LIBRARY_VERSION = "14.0.0-rc.44";
4
+ export declare const LIBRARY_VERSION = "14.0.0-rc.46";
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: "14.0.0-rc.44";
36
+ readonly library: "14.0.0-rc.46";
37
37
  readonly adcp: "3.2.0-rc.4";
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.2.0-rc.4", "3.2-rc.4"];
39
- readonly generatedAt: "2026-09-22T10:18:01.112Z";
39
+ readonly generatedAt: "2026-09-22T18:08:14.356Z";
40
40
  };
41
41
  /**
42
42
  * Get the AdCP specification version this library is built for
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * AdCP SDK library version
3
3
  */
4
- export declare const LIBRARY_VERSION = "14.0.0-rc.44";
4
+ export declare const LIBRARY_VERSION = "14.0.0-rc.46";
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: "14.0.0-rc.44";
36
+ readonly library: "14.0.0-rc.46";
37
37
  readonly adcp: "3.2.0-rc.4";
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.2.0-rc.4", "3.2-rc.4"];
39
- readonly generatedAt: "2026-09-22T10:18:01.112Z";
39
+ readonly generatedAt: "2026-09-22T18:08:14.356Z";
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 = "14.0.0-rc.44";
34
+ const LIBRARY_VERSION = "14.0.0-rc.46";
35
35
  const ADCP_VERSION = "3.2.0-rc.4";
36
36
  const ADCP_MAJOR_VERSION = 3;
37
37
  const COMPATIBLE_ADCP_VERSIONS = [
@@ -89,10 +89,10 @@ const COMPATIBLE_ADCP_VERSIONS = [
89
89
  "3.2-rc.4"
90
90
  ];
91
91
  const VERSION_INFO = {
92
- library: "14.0.0-rc.44",
92
+ library: "14.0.0-rc.46",
93
93
  adcp: "3.2.0-rc.4",
94
94
  compatibleVersions: COMPATIBLE_ADCP_VERSIONS,
95
- generatedAt: "2026-09-22T10:18:01.112Z"
95
+ generatedAt: "2026-09-22T18:08:14.356Z"
96
96
  };
97
97
  function getAdcpVersion() {
98
98
  return ADCP_VERSION;
@@ -1,4 +1,4 @@
1
- const LIBRARY_VERSION = "14.0.0-rc.44";
1
+ const LIBRARY_VERSION = "14.0.0-rc.46";
2
2
  const ADCP_VERSION = "3.2.0-rc.4";
3
3
  const ADCP_MAJOR_VERSION = 3;
4
4
  const COMPATIBLE_ADCP_VERSIONS = [
@@ -56,10 +56,10 @@ const COMPATIBLE_ADCP_VERSIONS = [
56
56
  "3.2-rc.4"
57
57
  ];
58
58
  const VERSION_INFO = {
59
- library: "14.0.0-rc.44",
59
+ library: "14.0.0-rc.46",
60
60
  adcp: "3.2.0-rc.4",
61
61
  compatibleVersions: COMPATIBLE_ADCP_VERSIONS,
62
- generatedAt: "2026-09-22T10:18:01.112Z"
62
+ generatedAt: "2026-09-22T18:08:14.356Z"
63
63
  };
64
64
  function getAdcpVersion() {
65
65
  return ADCP_VERSION;
@@ -1,7 +1,7 @@
1
1
  # AdCP Type Summary
2
2
 
3
3
  > Generated at: 2026-09-22
4
- > @adcp/sdk v14.0.0-rc.44
4
+ > @adcp/sdk v14.0.0-rc.46
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
 
@@ -109,6 +109,8 @@ The helper only returns `definitive: true` when all of these conditions hold:
109
109
 
110
110
  An omitted expected-period denominator can still diagnose delivery, but can never prove completeness. Pass `[]` only when the buyer independently knows that no periods are expected in the requested scope.
111
111
 
112
+ A unique official revision takes precedence over a retained snapshot even without an explicit supersession link. The logical-slice join includes owned revisions whose destination materialization is still pending. In that case the result names the official revision, remains nondefinitive with `MISSING_VERIFIED_MATERIALIZATION`, and does not inspect or receipt the older snapshot. Forks, cycles, multiple official revisions, incomplete history, and mismatched ownership still prevent definitive reconciliation; the seller's retained records are preserved.
113
+
112
114
  ```ts
113
115
  import {
114
116
  createHttpsReportingResourceReader,
package/docs/llms.txt CHANGED
@@ -1,7 +1,7 @@
1
1
  # Ad Context Protocol (AdCP)
2
2
 
3
3
  > Generated at: 2026-09-22
4
- > Library: @adcp/sdk v14.0.0-rc.44
4
+ > Library: @adcp/sdk v14.0.0-rc.46
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.
@@ -11,8 +11,8 @@ below as the publication/deployment gate.
11
11
 
12
12
  | Fact | Value |
13
13
  | --- | --- |
14
- | Exact npm package | `@adcp/sdk@14.0.0-rc.44` |
15
- | npm integrity | registry-derived after publication; run `npm view @adcp/sdk@14.0.0-rc.44 dist.integrity` |
14
+ | Exact npm package | `@adcp/sdk@14.0.0-rc.46` |
15
+ | npm integrity | registry-derived after publication; run `npm view @adcp/sdk@14.0.0-rc.46 dist.integrity` |
16
16
  | Node.js runtime | `^20.19.0 || >=22.12.0` |
17
17
  | Default AdCP wire release | `3.2.0-rc.4` |
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.2.0-rc.4`, `3.2-rc.4` |
@@ -21,8 +21,8 @@ below as the publication/deployment gate.
21
21
  Install exact production inputs rather than a moving prerelease range:
22
22
 
23
23
  ```bash
24
- npm install --save-exact '@adcp/sdk@14.0.0-rc.44'
25
- npm view '@adcp/sdk@14.0.0-rc.44' dist.integrity
24
+ npm install --save-exact '@adcp/sdk@14.0.0-rc.46'
25
+ npm view '@adcp/sdk@14.0.0-rc.46' dist.integrity
26
26
  ```
27
27
 
28
28
  ### Required and optional peers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcp/sdk",
3
- "version": "14.0.0-rc.44",
3
+ "version": "14.0.0-rc.46",
4
4
  "description": "AdCP SDK — client, server, and compliance harnesses for the AdContext Protocol (MCP + A2A)",
5
5
  "workspaces": [
6
6
  ".",