Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
13 changes: 13 additions & 0 deletions .changeset/reporting-subscription-coverage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@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 published offerings (matching reads each offering's `standard_dimensions` and `unmapped_dimensions`), the readback's `inactive_intervals`, and the published `subscription_coverage` exceptions, including per-offering `gaps[]` and `timezone_cutovers[]` with `previous_timezone`. It returns every owed (buy, offering, period), and `groupExpectedSubscriptionPeriodsV1` groups them into the obligations that must carry them. It implements the start rule of adcp#8593 (the first boundary at or after the later of `accepted_at` and `authorized_at`, and every period ending after the buy's `confirmed_at`, so the period in which a buy is confirmed is owed in full), 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.

`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 rules follow adcontextprotocol/adcp#8585. The fields it added after 3.3.0-beta.1 (`standard_dimensions`, `unmapped_dimensions`, `inactive_intervals`, `previous_timezone`, and per-offering `offering_id` on gaps and cutovers) are hand-modeled until 3.3.0-beta.2 ships. The cross-SDK parity table is `test/fixtures/reporting-subscriptions/coverage-cases.json`.

Sub-daily periods under a local alignment restart at each local midnight on wall-clock multiples (adcontextprotocol/adcp#8588). A gap yields no boundary and a fold one per offset, and the duration must divide 24 hours; mixed date and time components are refused. Calendars gain `next()`. An empty flight overlaps nothing, and a cutover carries the outgoing stretch's own start, so two cutovers in one period never owe a period twice. A billing anchor inside a DST fold is interval zero itself. A wire `authorized_at` equal to the chain start continues the chain. The walk has an iteration budget. The SDK's schema validation accepts dimension values outside the published `enums/reporting-dimension.json` vocabulary, which is extensible.
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