Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/reporting-subscription-coverage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@adcp/sdk': minor
---

Add the buyer-computable coverage rule for AdCP 3.3 principal-level reporting subscriptions (experimental `media_buy.reporting_subscriptions`, #3504). `expectedSubscriptionPeriods(input)`, exported from `@adcp/sdk/reporting/consumer` and `@adcp/sdk/reporting/ledger`, is a pure function of the buyer's own records: subscription versions with their `accepted_at`, visible confirmed buys and their earlier flight values, the account's grants and timezone, the seller's offerings with their standard-dimension mapping, and the published `subscription_coverage` exceptions. It returns every owed (buy, offering, period), and `groupExpectedSubscriptionPeriodsV1` groups them into the obligations that must carry them. It implements the start rule (first boundary at or after the latest of `accepted_at`, `confirmed_at` and `authorized_at`), flight overlap as of each period's resolution, unclipped periods, the single cutover rule for new versions, deactivation, resolution changes and account timezone changes, revocation and re-grant with and without `backfill_on_regrant`, and exceptions that only remove periods.

Also exported: `createReportingPeriodCalendarV1`, the normative `period_generation` calendar for `utc`, `account_timezone`, `source_timezone` and `billing_cycle` schedules; `resolveReportingSubscriptionOfferingsV1` for `exact`, `finest` and `all` selector resolution with byte-order ties; `reportingSubscriptionScopeIncludesV1`; and `reportingConfigurationCapabilitiesV1`, which builds `configuration_tasks`, the deprecated `configuration_task` and the `subscriptions` block consistently. `createAdcpServerFromPlatform` adds `media_buy.reporting_subscriptions` to `experimental_features` when the reporting capability carries a `subscriptions` block; without one the list is unchanged.

`installConfiguration` now throws a typed `ReportingConfigurationImmutableError` (`code: 'REPORTING_CONFIGURATION_IMMUTABLE'`, with `accountId`, `deliveryConfigId` and `deliveryConfigVersion`) when an existing generation is reinstalled with different content. The message is unchanged; use `isReportingConfigurationImmutableError` instead of matching it.

The cross-SDK parity table is `test/fixtures/reporting-subscriptions/coverage-cases.json`.
3 changes: 2 additions & 1 deletion scripts/check-package-size.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,8 @@ const MAX_PACKED_FILE_COUNT =
13 + // Property-list identifier, server, and certification modules (three dual-format sets) and adoption guide.
4 + // Property-list reference probe (one dual-format runtime/declaration module set).
5 + // Creative build probe: one dual-format runtime/declaration module (+4) and one guide (+1).
1840; // SDK 17: 3.3 schemas/compliance, per-tool type entrypoints and migration/release notes.
1840 + // SDK 17: 3.3 schemas/compliance, per-tool type entrypoints and migration/release notes.
20; // Reporting subscription calendar, selection, coverage, capabilities and index (five dual-format sets).
const MAX_BUNDLED_SCHEMA_BYTES = 2000 * 1024;
const MAX_CJS_SCHEMA_DECLARATION_BYTES = 45 * 1024 * 1024;
const MAX_ESM_SCHEMA_FACADE_BYTES = 1024;
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-reporting-consumer-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ try {
`
const assert = require('node:assert/strict');
const buyer = require('@adcp/sdk/reporting/consumer');
for (const name of ['reconcileReporting', 'reconcileReportingCoreV1', 'detectReportingContentMismatch', 'isReportingCoverageEvidence', 'createPostgresReportingConsumerRuntimeV1', 'createReliableReportingConsumerV1']) {
for (const name of ['reconcileReporting', 'reconcileReportingCoreV1', 'detectReportingContentMismatch', 'isReportingCoverageEvidence', 'createPostgresReportingConsumerRuntimeV1', 'createReliableReportingConsumerV1', 'expectedSubscriptionPeriods']) {
assert.equal(typeof buyer[name], 'function', name);
}
assert.equal(buyer.createReliableReportingServiceV1, undefined);
Expand All @@ -49,7 +49,7 @@ try {
`
import assert from 'node:assert/strict';
import * as buyer from '@adcp/sdk/reporting/consumer';
for (const name of ['reconcileReporting', 'reconcileReportingCoreV1', 'detectReportingContentMismatch', 'isReportingCoverageEvidence', 'createPostgresReportingConsumerRuntimeV1', 'createReliableReportingConsumerV1']) {
for (const name of ['reconcileReporting', 'reconcileReportingCoreV1', 'detectReportingContentMismatch', 'isReportingCoverageEvidence', 'createPostgresReportingConsumerRuntimeV1', 'createReliableReportingConsumerV1', 'expectedSubscriptionPeriods']) {
assert.equal(typeof buyer[name], 'function', name);
}
assert.equal(buyer.createReliableReportingServiceV1, undefined);
Expand Down
3 changes: 3 additions & 0 deletions src/lib/reporting/consumer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,6 @@ export type {
ReportingResourceReadRole,
ReportingResourceReader,
} from '../inspection';

// Principal-level reporting subscriptions (experimental, AdCP 3.3): buyer-computable coverage.
export * from '../subscriptions';
2 changes: 2 additions & 0 deletions src/lib/reporting/ledger/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export * from './row-encoding';
export * from './row-storage-errors';
export * from './receipts';
export * from './offering-validation';
// Principal-level reporting subscriptions (experimental, AdCP 3.3): the shared calendar, selection and coverage rule.
export * from '../subscriptions';

// Building blocks a ledger store implementation shares with the protocol layer. They are exported so a
// store can live outside the SDK without importing internals.
Expand Down
7 changes: 6 additions & 1 deletion src/lib/reporting/ledger/producer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
retryReportingStatusNotificationsV1,
} from './lifecycle';
import { listAdjustmentMetadataFromStore, listRevisionMetadataFromStore } from './revision-metadata';
import { ReportingConfigurationImmutableError } from './types';
import type {
CreateReportingProducerOptionsV1,
ReportingLedgerAdjustmentMetadataV1,
Expand Down Expand Up @@ -110,7 +111,11 @@ export function createReportingProducer(options: CreateReportingProducerOptionsV
}
}
if (!replayFingerprints.includes(replay.semanticFingerprint)) {
throw new Error('Reporting configuration generation is immutable');
throw new ReportingConfigurationImmutableError({
accountId: normalizedInput.account.account_id,
deliveryConfigId: normalizedInput.delivery_config_id,
deliveryConfigVersion: normalizedInput.delivery_config_version,
});
}
if (installOptions.historyStart !== undefined) {
const requested = historyInstant(installOptions.historyStart);
Expand Down
32 changes: 32 additions & 0 deletions src/lib/reporting/ledger/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,38 @@ export class ReportingLedgerContinuityError extends Error {
}
}

/**
* `installConfiguration` was asked to install an existing `(delivery_config_id,
* delivery_config_version)` generation with different semantic content. A generation is
* immutable: install a higher `delivery_config_version` instead.
*
* The message is unchanged from earlier releases, so hosts that matched on it keep working;
* match on `code` (or `instanceof`, or `name` across SDK copies) instead.
*/
export class ReportingConfigurationImmutableError extends Error {
readonly code = 'REPORTING_CONFIGURATION_IMMUTABLE' as const;
readonly accountId: string;
readonly deliveryConfigId: string;
readonly deliveryConfigVersion: number;
constructor(details: { accountId: string; deliveryConfigId: string; deliveryConfigVersion: number }) {
super('Reporting configuration generation is immutable');
this.name = 'ReportingConfigurationImmutableError';
this.accountId = details.accountId;
this.deliveryConfigId = details.deliveryConfigId;
this.deliveryConfigVersion = details.deliveryConfigVersion;
}
}

/** Whether an error is a {@link ReportingConfigurationImmutableError}, including one from another SDK copy. */
export function isReportingConfigurationImmutableError(error: unknown): error is ReportingConfigurationImmutableError {
return (
error instanceof ReportingConfigurationImmutableError ||
(error instanceof Error &&
error.name === 'ReportingConfigurationImmutableError' &&
(error as { code?: unknown }).code === 'REPORTING_CONFIGURATION_IMMUTABLE')
);
}

export class ReportingLedgerSnapshotUnavailableError extends Error {
constructor() {
super('Reporting ledger snapshot is unavailable');
Expand Down
6 changes: 6 additions & 0 deletions src/lib/reporting/service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1561,5 +1561,11 @@ function deepFreeze<T>(value: T): T {
return value;
}

export { ReportingConfigurationImmutableError, isReportingConfigurationImmutableError } from '../ledger';
export {
reportingConfigurationCapabilitiesV1,
type ReportingConfigurationCapabilitiesInputV1,
type ReportingConfigurationCapabilitiesV1,
} from '../subscriptions/capabilities';
export * from './conformance';
export * from './scheduler-support';
Loading
Loading