diff --git a/.changeset/catalog-optional-collection-features.md b/.changeset/catalog-optional-collection-features.md
new file mode 100644
index 00000000000..19cddd56f97
--- /dev/null
+++ b/.changeset/catalog-optional-collection-features.md
@@ -0,0 +1,9 @@
+---
+"adcontextprotocol": patch
+---
+
+Collection selection and owner-sold carriage checks now discover native wholesale sandbox products instead of seeding invented collection support. Six independent scopes cover selectable subsets, one-pair default materialization, fixed bundles (omitted or false targeting flag), and structural cross-publisher selection. A completed valid catalog with no qualifying product reports `not_applicable` (`fixture_unsatisfied`): the feature was not tested, not passed or proven unsupported. Discovery and selected-path errors still fail.
+
+Selectors, negative witnesses and commercial terms come from the same selected product/price graph. Exact echoes compare domain-qualified sets without assuming illustrative collection names, domains or prices. Two separately tool-gated companions retain persisted subset/default checks without making optional `get_media_buys` support a prerequisite for creation. Only non-guaranteed, fixed-price CPM catalog products are covered; seeded-only and brief-only support remains outside this evidence.
+
+Completes #8254 with the reporting correction in #8336. No seller capability field, controller seed-refusal rule or released artifact is changed.
diff --git a/docs/reference/known-limitations.mdx b/docs/reference/known-limitations.mdx
index 56556d384a8..4e5498d5188 100644
--- a/docs/reference/known-limitations.mdx
+++ b/docs/reference/known-limitations.mdx
@@ -84,7 +84,9 @@ Request-specific configured offers are not a gap: canonical products carry no `i
- **Catalog reporting selection.** `advanced_delivery_reporting` selects a native wholesale sandbox product advertising format breakdown; `time_based_delivery_reporting` independently selects one listing `time_based_views`.
- **Catalog reporting not applicable.** `fixture_unsatisfied`, projected `not_applicable`, means no qualifying product was found. The feature was not tested; this is neither a pass nor proof of nonsupport. Fixture discovery errors and broken selected support still fail.
- **Catalog reporting pass scope.** A pass covers required response fields, metric narrowing and legal returned measurements. A pass does not attest that the seller returns time-based views; the controller cannot inject view observations. A one-row format request cannot prove multi-row ordering or selection from an unobserved population. Mature metric value-or-disclosure accountability remains [#8412](https://github.com/adcontextprotocol/adcp/issues/8412).
-- **Catalog reporting untested paths.** Only non-guaranteed, fixed-price CPM products are graded. Guaranteed, auction-priced, brief-only and seeded-only support is not tested, and the card cannot yet distinguish this from nonsupport ([#8416](https://github.com/adcontextprotocol/adcp/issues/8416)). Collection scenarios are unchanged. The scheduled heartbeat runs bare `comply()` without selected-price budget or authored-brand derivation until [SDK #3381](https://github.com/adcontextprotocol/adcp-client/issues/3381) and [SDK #3382](https://github.com/adcontextprotocol/adcp-client/issues/3382) are addressed. Adapter and SDK details are in the [qualification memo](https://github.com/adcontextprotocol/adcp/blob/main/specs/optional-product-reporting-qualification.md).
+- **Catalog reporting untested paths.** Only non-guaranteed, fixed-price CPM products are graded. Guaranteed, auction-priced, brief-only and seeded-only support is not tested, and the card cannot yet distinguish this from nonsupport ([#8416](https://github.com/adcontextprotocol/adcp/issues/8416)). The scheduled heartbeat runs bare `comply()` without selected-price budget or authored-brand derivation until [SDK #3381](https://github.com/adcontextprotocol/adcp-client/issues/3381) and [SDK #3382](https://github.com/adcontextprotocol/adcp-client/issues/3382) are addressed. Adapter and SDK details are in the [qualification memo](https://github.com/adcontextprotocol/adcp/blob/main/specs/optional-product-reporting-qualification.md).
+- **Catalog collection coverage.** Six independently qualified scopes grade selectable subsets, one-pair default materialization, fixed-bundle enforcement and restatement, and structural cross-publisher carriage/full or subset selection. Fixed bundles accept literal false or an omitted flag. A valid complete no-match is `fixture_unsatisfied`: feature not tested, never a support pass or proof of nonsupport. Fixture catalog discovery errors and broken selected paths still fail. Two separately reader-gated companions check persisted subset/default echoes; absence of `get_media_buys` does not suppress core creation checks.
+- **Collection coverage boundaries.** Only native wholesale sandbox, non-guaranteed fixed-price CPM products are exercised; seeded-only, brief-only, guaranteed and auction-priced support remains outside this catalog slice. Cross-publisher means a collection namespace outside every property namespace on the same product; it does not verify ownership, host authorization or a supply path. Standalone reads require trusted successful-create buy/package/product/committed-selector context and requalify the current catalog; no durable cross-request grant or catalog-retirement immunity is claimed. SDK 16.3 can falsely fail an active read after a legal create response omitting optional `media_buy_status` ([SDK #3395](https://github.com/adcontextprotocol/adcp-client/issues/3395)); that variation remains unqualified, and the optional field is not made mandatory. Widened sets still fail, but failed comparison evidence omits the authored context key ([SDK #3396](https://github.com/adcontextprotocol/adcp-client/issues/3396)). Wholesale feed changes between pages are not yet restarted by the buyer ([SDK #3397](https://github.com/adcontextprotocol/adcp-client/issues/3397)); mutable-feed qualification remains incomplete, not seller nonconformance. The scheduled bare `comply()` path still lacks the repository caller's commercial/selector projection until [SDK #3382](https://github.com/adcontextprotocol/adcp-client/issues/3382). Details and historical #8259 reconciliation are in the [collection qualification memo](https://github.com/adcontextprotocol/adcp/blob/main/specs/optional-product-collection-qualification.md).
- **OOH/audio journey coverage remains bounded.** Expanded OOH/audio measurement identities still have schema and focused contract-test coverage rather than one complete discovery → purchase → delivery journey per metric family.
- **Audience activation coverage.** Audience activation has capability-gated storyboards for both discovery and execution: `audience_activation_discovery` covers the catalog-wide union, per-product declarations, vendor matching, OR/wildcard filter semantics, and exclusion of undeclared products; `external_audience_source_binding` binds a declared shared dataset, preserves its source and match state through discovery, uses the resulting audience in a media buy, and rejects undeclared rails or cross-transport updates. The reference seller deliberately advertises `dataset_query`, not `platform_distribution`, so the latter remains not applicable rather than simulated as a capability it does not provide.
- **No automated enforcement of platform agnosticism** beyond the `check:platform-agnostic` lint scanning property names. A richer check covering schema semantics is future work.
diff --git a/package.json b/package.json
index 08f7f3000d6..2d94ec2ea85 100644
--- a/package.json
+++ b/package.json
@@ -125,6 +125,7 @@
"test:tenant-routing": "vitest run --config server/vitest.config.ts server/src/training-agent/tenants/tenant-smoke.test.ts",
"test:format-identity-boundaries": "node --test --test-force-exit --test-timeout=30000 tests/lint-format-identity-boundaries.test.cjs",
"test:sdk-runner-capability-gates": "node --test --test-force-exit --test-timeout=30000 tests/sdk-runner-capability-gates.test.cjs && node --test --test-timeout=30000 tests/evaluator-allowlist-runner.test.cjs && node --test --test-timeout=30000 tests/optional-product-reporting.test.cjs tests/optional-product-reporting-public.test.cjs tests/catalog-reporting-preflight.test.cjs tests/catalog-reporting-preflight-security.test.cjs",
+ "test:optional-product-collections": "node --test --test-concurrency=1 --test-timeout=30000 tests/optional-product-collection-discovery.test.cjs tests/optional-product-collection-echo.test.cjs tests/optional-product-collection-price.test.cjs tests/optional-product-collection-rejections.test.cjs tests/optional-product-collection-scope.test.cjs tests/optional-product-collections.test.cjs tests/optional-product-collection-history.test.cjs",
"test:callapi-state-change": "node --test --test-force-exit --test-timeout=30000 tests/lint-callapi-state-change.test.cjs",
"test:cdn-artifacts-worker": "node --test --test-force-exit --test-timeout=30000 tests/artifact-cdn-worker.test.cjs",
"test:sign-protocol-tarball": "node --test --test-force-exit --test-timeout=30000 tests/sign-protocol-tarball.test.cjs",
@@ -171,7 +172,7 @@
"audit:verification-profile-shadow": "tsx server/src/scripts/audit-verification-profile-shadow.ts",
"test:schema-utf8": "node scripts/normalize-schema-utf8.mjs --check",
"fix:schema-utf8": "node scripts/normalize-schema-utf8.mjs",
- "test": "npm run test:sdk-response-conformance && npm run test:docs-nav && npm run test:owned-links && npm run test:release-docs-nav && npm run test:rewrite-dist-redirect-links && npm run test:rewrite-dist-links-idempotency && npm run test:docs-error-handling-copy && npm run test:schemas && npm run test:security-profiles && npm run test:performance-feedback && npm run test:mcp-schema-projection && npm run test:dist-schema-version-ids && npm run test:examples && npm run test:extensions && npm run test:extension-schemas && npm run test:error-handling && npm run test:json-schema && npm run test:audio-radio && npm run test:adagents-catalog-only && npm run test:fast-channel-collections && npm run test:collection-selection-echo && npm run test:canonical-reference-resolver && npm run test:composed && npm run test:rejection-arm-mutex && npm run test:migrations && npm run test:hmac-vectors && npm run test:hmac-signer-conformance && npm run test:webhook-signing-vectors && npm run test:functional-request-signing-contract && npm run test:webhook-receiver-envelope && npm run test:webhook-emission-storyboard && npm run test:oauth-setup-vectors && npm run test:a2a-profile-extension && npm run test:transport-errors && npm run test:wba-profile-vectors && npm run test:targeting-overlay-vectors && npm run test:targeting-null-clear && npm run test:targeting-aware-discovery && npm run test:demographic-targeting && npm run test:audience-activation-methods && npm run test:principal && npm run test:audience-external-sources && npm run test:language-targeting && npm run test:availability-horizon && npm run test:attestation-vectors && npm run test:rights-attestations && npm run test:governance-runtime-attestations && npm run test:governance-conditions-storyboard && npm run test:signal-governance-storyboard && npm run test:governance-task-slices && npm run test:audience-evidence && npm run test:status-as-of-vectors && npm run test:storyboard-scoping && npm run test:storyboard-branch-sets && npm run test:storyboard-provides-state-for && npm run test:storyboard-fixture-resolution && npm run test:storyboard-contradictions && npm run test:storyboard-context-entity && npm run test:storyboard-auth-shape && npm run test:storyboard-test-kits && npm run test:storyboard-buyer-fixture-lifecycle-tools && npm run test:storyboard-buyer-fixture-products && npm run test:compliance-packaged-refs && npm run test:compliance-source-authority && npm run test:storyboard-sample-request-schema && npm run test:storyboard-response-schema && npm run test:storyboard-context-output-paths && npm run test:storyboard-context-resolution && npm run test:storyboard-validations-paths && npm run test:storyboard-check-enum && npm run test:update-media-buy-affected-packages && npm run test:storyboard-advisory-expiry && npm run test:storyboard-controller-requires && npm run test:storyboard-raw-mode-required && npm run test:storyboard-upstream-traffic-paths && npm run test:storyboard-generate-phase-scope && npm run test:refine-finalize-validation-ids && npm run test:run-storyboards-schema-root && npm run test:storyboard-doc-parity && npm run test:v4-compatibility-registry && npm run test:pagination-invariant && npm run test:version-envelope && npm run test:test-dynamic-imports && npm run test:sdk-shims && npm run test:format-identity-boundaries && npm run test:sdk-runner-capability-gates && npm run test:callapi-state-change && npm run test:sign-protocol-tarball && npm run test:chat-streaming-code-fences && npm run test:certification-demo-formatting && npm run test:build-schemas-hoist-enums && npm run test:build-schemas-hoist-marked && npm run test:build-schemas-hoist-by-identity && npm run test:build-schemas-async-response-refs && npm run test:build-schemas-vendor-metric-uniqueness && npm run test:release-workflow && npm run test:immutable-release-artifacts && npm run test:patch-3-0-compat-bundle && npm run test:error-codes && npm run test:compliance-snippets && npm run test:doc-compliance-drift && npm run test:substitution-vector-names && npm run test:platform-agnostic && npm run test:oneof-discriminators && npm run test:schema-utf8 && npm run test:unit && npm run test:server-unit && npm run test:openapi && npm run typecheck && npm run test:contracts && npm run test:example-validation && npm run test:test-coverage",
+ "test": "npm run test:sdk-response-conformance && npm run test:docs-nav && npm run test:owned-links && npm run test:release-docs-nav && npm run test:rewrite-dist-redirect-links && npm run test:rewrite-dist-links-idempotency && npm run test:docs-error-handling-copy && npm run test:schemas && npm run test:security-profiles && npm run test:performance-feedback && npm run test:mcp-schema-projection && npm run test:dist-schema-version-ids && npm run test:examples && npm run test:extensions && npm run test:extension-schemas && npm run test:error-handling && npm run test:json-schema && npm run test:audio-radio && npm run test:adagents-catalog-only && npm run test:fast-channel-collections && npm run test:collection-selection-echo && npm run test:canonical-reference-resolver && npm run test:composed && npm run test:rejection-arm-mutex && npm run test:migrations && npm run test:hmac-vectors && npm run test:hmac-signer-conformance && npm run test:webhook-signing-vectors && npm run test:functional-request-signing-contract && npm run test:webhook-receiver-envelope && npm run test:webhook-emission-storyboard && npm run test:oauth-setup-vectors && npm run test:a2a-profile-extension && npm run test:transport-errors && npm run test:wba-profile-vectors && npm run test:targeting-overlay-vectors && npm run test:targeting-null-clear && npm run test:targeting-aware-discovery && npm run test:demographic-targeting && npm run test:audience-activation-methods && npm run test:principal && npm run test:audience-external-sources && npm run test:language-targeting && npm run test:availability-horizon && npm run test:attestation-vectors && npm run test:rights-attestations && npm run test:governance-runtime-attestations && npm run test:governance-conditions-storyboard && npm run test:signal-governance-storyboard && npm run test:governance-task-slices && npm run test:audience-evidence && npm run test:status-as-of-vectors && npm run test:storyboard-scoping && npm run test:storyboard-branch-sets && npm run test:storyboard-provides-state-for && npm run test:storyboard-fixture-resolution && npm run test:storyboard-contradictions && npm run test:storyboard-context-entity && npm run test:storyboard-auth-shape && npm run test:storyboard-test-kits && npm run test:storyboard-buyer-fixture-lifecycle-tools && npm run test:storyboard-buyer-fixture-products && npm run test:compliance-packaged-refs && npm run test:compliance-source-authority && npm run test:storyboard-sample-request-schema && npm run test:storyboard-response-schema && npm run test:storyboard-context-output-paths && npm run test:storyboard-context-resolution && npm run test:storyboard-validations-paths && npm run test:storyboard-check-enum && npm run test:update-media-buy-affected-packages && npm run test:storyboard-advisory-expiry && npm run test:storyboard-controller-requires && npm run test:storyboard-raw-mode-required && npm run test:storyboard-upstream-traffic-paths && npm run test:storyboard-generate-phase-scope && npm run test:refine-finalize-validation-ids && npm run test:run-storyboards-schema-root && npm run test:storyboard-doc-parity && npm run test:v4-compatibility-registry && npm run test:pagination-invariant && npm run test:version-envelope && npm run test:test-dynamic-imports && npm run test:sdk-shims && npm run test:format-identity-boundaries && npm run test:sdk-runner-capability-gates && npm run test:optional-product-collections && npm run test:callapi-state-change && npm run test:sign-protocol-tarball && npm run test:chat-streaming-code-fences && npm run test:certification-demo-formatting && npm run test:build-schemas-hoist-enums && npm run test:build-schemas-hoist-marked && npm run test:build-schemas-hoist-by-identity && npm run test:build-schemas-async-response-refs && npm run test:build-schemas-vendor-metric-uniqueness && npm run test:release-workflow && npm run test:immutable-release-artifacts && npm run test:patch-3-0-compat-bundle && npm run test:error-codes && npm run test:compliance-snippets && npm run test:doc-compliance-drift && npm run test:substitution-vector-names && npm run test:platform-agnostic && npm run test:oneof-discriminators && npm run test:schema-utf8 && npm run test:unit && npm run test:server-unit && npm run test:openapi && npm run typecheck && npm run test:contracts && npm run test:example-validation && npm run test:test-coverage",
"test:all": "npm run test:schemas && npm run test:examples && npm run test:extensions && npm run test:error-handling && npm run test:snippets && npm run typecheck",
"precommit:server-unit": "node scripts/precommit-server-unit.cjs",
"precommit": "bash scripts/with-timeout.sh 300 npm run test:unit && npm run test:test-dynamic-imports && npm run test:format-identity-boundaries && npm run test:callapi-state-change && bash scripts/with-timeout.sh 1500 npm run precommit:server-unit && npm run typecheck",
diff --git a/scripts/lint-storyboard-fixture-resolution.cjs b/scripts/lint-storyboard-fixture-resolution.cjs
index dff099eb941..14271715402 100644
--- a/scripts/lint-storyboard-fixture-resolution.cjs
+++ b/scripts/lint-storyboard-fixture-resolution.cjs
@@ -15,6 +15,7 @@ const OPERATORS = new Set([
'contains_all',
'any_match',
'canonical_format_satisfies',
+ 'collection_selection_satisfies',
]);
const ID_FIELDS = {
accounts: 'account_id',
@@ -63,7 +64,7 @@ function lintMatchClause(clause, at, violations) {
violations.push({ rule: 'match_clause_shape', at });
return;
}
- if (typeof clause.path !== 'string' || !clause.path.startsWith('/')) {
+ if (typeof clause.path !== 'string' || (clause.path !== '' && !clause.path.startsWith('/'))) {
violations.push({ rule: 'match_path', at });
}
if (!OPERATORS.has(clause.operator)) {
@@ -87,6 +88,17 @@ function lintMatchClause(clause, at, violations) {
if (!Object.hasOwn(clause, 'value') || Object.hasOwn(clause, 'where')) {
violations.push({ rule: 'match_value_shape', at });
}
+ // Keep the merged #8376 operand closed, including provisional spellings.
+ if (clause.operator === 'collection_selection_satisfies') {
+ const value = clause.value;
+ if (!value || typeof value !== 'object' || Array.isArray(value) ||
+ Object.keys(value).some(key => !['collection_targeting', 'minimum_pairs', 'require_cross_publisher_collection'].includes(key)) ||
+ !['fixed', 'selectable', 'any'].includes(value.collection_targeting) ||
+ (value.minimum_pairs !== undefined && ![1, 2].includes(value.minimum_pairs)) ||
+ (value.require_cross_publisher_collection !== undefined && typeof value.require_cross_publisher_collection !== 'boolean')) {
+ violations.push({ rule: 'collection_selection_value', at });
+ }
+ }
if (clause.operator === 'contains_all' && !Array.isArray(clause.value)) {
violations.push({ rule: 'contains_all_value', at });
}
diff --git a/scripts/lint-storyboard-sample-request-schema.cjs b/scripts/lint-storyboard-sample-request-schema.cjs
index 9bb68f214f8..c738f3d787b 100644
--- a/scripts/lint-storyboard-sample-request-schema.cjs
+++ b/scripts/lint-storyboard-sample-request-schema.cjs
@@ -167,6 +167,17 @@ const SHA256_DIGEST_PLACEHOLDER = `sha256:${'A'.repeat(43)}`;
* The `depth` guard prevents infinite recursion on self-referential schemas.
*/
function placeholderFor(schema, depth = 0) {
+ // Whole-value witnesses can contain selectors composed with allOf. Merge
+ // required object fields for placeholders only; AJV validates the full schema.
+ if (Array.isArray(schema?.allOf)) {
+ const branches = schema.allOf.map(branch => resolveSchemaNode(branch));
+ if (branches.every(branch => branch && (branch.type === 'object' || branch.properties || branch.required))) {
+ return synthesizeObject({
+ required: [...new Set(branches.flatMap(branch => branch.required ?? []))],
+ properties: Object.assign({}, ...branches.map(branch => branch.properties ?? {})),
+ }, depth);
+ }
+ }
const resolved = resolveSchemaNode(schema);
if (!resolved) return STRING_PLACEHOLDER;
const types = Array.isArray(resolved.type) ? resolved.type : [resolved.type];
@@ -193,7 +204,10 @@ function placeholderFor(schema, depth = 0) {
if (types.includes('integer')) return resolved.minimum ?? 1;
if (types.includes('number')) return resolved.minimum ?? 0;
if (types.includes('boolean')) return true;
- if (types.includes('array')) return [];
+ if (types.includes('array')) {
+ if (depth > 4) return [];
+ return Array.from({ length: resolved.minItems ?? 0 }, () => placeholderFor(resolved.items, depth + 1));
+ }
if (types.includes('object')) {
// A discriminated union written as `{ type: "object", discriminator, oneOf: [...] }`
// (e.g. core/format-option-ref.json) carries no top-level `properties`, so
diff --git a/server/src/addie/mcp/member-tools.ts b/server/src/addie/mcp/member-tools.ts
index 71f56d94cb5..fa5102d6e54 100644
--- a/server/src/addie/mcp/member-tools.ts
+++ b/server/src/addie/mcp/member-tools.ts
@@ -5892,11 +5892,46 @@ export function createMemberToolHandlers(
output += `**Agent:** ${resolved.resolvedUrl}\n`;
output += `**Compliance target:** ${formatComplianceTarget(runTarget)}\n`;
const coverageGaps = result.coverage_gaps ?? [];
- const catalogReportingGap = ['media_buy_seller/advanced_delivery_reporting', 'media_buy_seller/time_based_delivery_reporting'].includes(storyboardId)
- && coverageGaps.some(gap => gap.reason === 'fixture_unsatisfied');
- const resultLabel = !result.overall_passed || result.failed_count > 0 ? 'FAILED'
- : catalogReportingGap ? 'NOT APPLICABLE — no qualifying product in your wholesale sandbox catalog (feature not tested)'
- : coverageGaps.length > 0 ? 'INCOMPLETE — coverage gap' : 'PASSED';
+ const catalogReportingGap =
+ [
+ 'media_buy_seller/advanced_delivery_reporting',
+ 'media_buy_seller/time_based_delivery_reporting',
+ 'media_buy_seller/collection_selection',
+ 'media_buy_seller/collection_default_materialization',
+ 'media_buy_seller/collection_selection_readback',
+ 'media_buy_seller/collection_default_materialization_readback',
+ 'media_buy_seller/fixed_bundle_enforcement',
+ 'media_buy_seller/fixed_bundle_restatement',
+ 'media_buy_seller/owner_sold_channel_carriage',
+ 'media_buy_seller/owner_sold_channel_subset',
+ ].includes(storyboardId) &&
+ coverageGaps.some((gap) => gap.reason === 'fixture_unsatisfied');
+ const collectionReaderAbsent =
+ [
+ 'media_buy_seller/collection_selection_readback',
+ 'media_buy_seller/collection_default_materialization_readback',
+ ].includes(storyboardId) &&
+ result.passed_count === 0 &&
+ result.phases.some(
+ (phase) =>
+ phase.phase_id === 'missing_tool' &&
+ phase.steps.some(
+ (step) =>
+ step.step_id === 'missing_tool' &&
+ step.skipped &&
+ step.skip_reason === 'missing_tool',
+ ),
+ );
+ const resultLabel =
+ !result.overall_passed || result.failed_count > 0
+ ? 'FAILED'
+ : catalogReportingGap
+ ? 'NOT APPLICABLE — no qualifying product in your wholesale sandbox catalog (feature not tested)'
+ : collectionReaderAbsent
+ ? 'NOT APPLICABLE — get_media_buys is not advertised (persisted collection readback not tested)'
+ : coverageGaps.length > 0
+ ? 'INCOMPLETE — coverage gap'
+ : 'PASSED';
output += `**Result:** ${resultLabel} — ${result.passed_count} passed, ${result.failed_count} failed, ${result.skipped_count} skipped, ${result.validations_advisory_failed ?? 0} advisory validation(s) failed\n`;
output += `**Duration:** ${(result.total_duration_ms / 1000).toFixed(1)}s\n\n`;
for (const gap of coverageGaps) {
@@ -6005,6 +6040,13 @@ export function createMemberToolHandlers(
if (readinessPrompt) return readinessPrompt;
const contextRefMeta = storyboardContextRefMeta(memberContext, storyboardId, resolved.resolvedUrl);
const context = resolveStoryboardInputContext(input.context, contextRefMeta);
+ // SDK captures may precede final validation. Preserve the last trusted
+ // continuation before the call, including when SDK code mutates its input.
+ const collectionReadback = [
+ 'media_buy_seller/collection_selection_readback',
+ 'media_buy_seller/collection_default_materialization_readback',
+ ].includes(storyboardId);
+ const previousContext = collectionReadback ? structuredClone(context) : context;
if (!hasExplicitComplianceTarget(input)) {
runTarget = await selectComplianceTargetForAgent(
@@ -6146,7 +6188,9 @@ export function createMemberToolHandlers(
}
}
- if (result.next) {
+ if (collectionReadback && !result.passed && !result.skipped && result.step_id === 'create_selection') {
+ output += `\nThe create did not pass. Fix it and re-run \`create_selection\` before attempting persisted readback. The context below retains the prior successful continuation only.\n`;
+ } else if (result.next) {
output += `\n### Next step\n`;
output += `**${result.next.title}** (\`${result.next.step_id}\`) — \`${result.next.task}\`\n`;
if (result.next.narrative) output += `${renderStoryboardDiagnostic(result.next.narrative, RUNNER_ERROR_MAX_LEN)}\n`;
@@ -6157,7 +6201,9 @@ export function createMemberToolHandlers(
// Include an opaque encrypted reference for the next step call so
// agent-controlled context cannot break out of this handoff surface.
- output += `\n\n${JSON.stringify(storeStoryboardContextRef(result.context, contextRefMeta), null, 2)}\n`;
+ const continuationContext = collectionReadback && (!result.passed || result.skipped)
+ ? previousContext : result.context;
+ output += `\n\n${JSON.stringify(storeStoryboardContextRef(continuationContext, contextRefMeta), null, 2)}\n`;
return output;
} catch (error) {
diff --git a/server/src/compliance/catalog-reporting-runner.ts b/server/src/compliance/catalog-reporting-runner.ts
index a0a46dfd28c..62f3c7159f0 100644
--- a/server/src/compliance/catalog-reporting-runner.ts
+++ b/server/src/compliance/catalog-reporting-runner.ts
@@ -1,11 +1,17 @@
/**
- * Buyer-side construction for the two catalog reporting storyboards.
+ * Buyer-side construction for catalog-qualified optional-feature storyboards.
*
* Arithmetic belongs to the caller: the closed storyboard language only
* substitutes whole context keys. Prepare the exact graph once, derive a
* budget from its selected price, and reuse that state without rediscovery.
* This is request-local preparation, not a cross-request grant/state store.
*/
+import {
+ createHmac,
+ randomBytes,
+ scryptSync,
+ timingSafeEqual,
+} from "node:crypto";
import {
PreparedFixturesDeniedError,
redactOAuthUrlForOutput,
@@ -17,6 +23,7 @@ import {
withPreparedFixtures,
withExternalSchemaRoot,
withServedReleaseSchemaRoots,
+ normalizeCollectionSelectors,
type PreparedStoryboardFixtures,
type PreparedFixturesAuthorizer,
type Storyboard,
@@ -31,7 +38,120 @@ const REPORTING_IDS = new Set([
"media_buy_seller/advanced_delivery_reporting",
"media_buy_seller/time_based_delivery_reporting",
]);
+export const COLLECTION_PRICE_CAPTURE = "__adcp_fixture_collection_price";
+export const COLLECTION_DECLARED_CAPTURE = "__adcp_fixture_collection_declared";
+export const COLLECTION_RELATION_CAPTURE = "__adcp_fixture_collection_relation";
+export const COLLECTION_BUDGET_CONTEXT = "catalog_collection_budget";
+export const COLLECTION_FULL_CONTEXT = "catalog_collection_full_selectors";
+export const COLLECTION_BULK_CONTEXT = "catalog_collection_bulk_selectors";
+const COLLECTION_IDS = new Set([
+ "media_buy_seller/collection_selection",
+ "media_buy_seller/collection_default_materialization",
+ "media_buy_seller/fixed_bundle_enforcement",
+ "media_buy_seller/fixed_bundle_restatement",
+ "media_buy_seller/owner_sold_channel_carriage",
+ "media_buy_seller/owner_sold_channel_subset",
+ "media_buy_seller/collection_selection_readback",
+ "media_buy_seller/collection_default_materialization_readback",
+]);
const BASELINE_BUDGET = 1000; // Buyer test intent, never a seller minimum.
+const COLLECTION_COMMITMENT = "catalog_collection_commitment_proof";
+// Match the existing hosted envelope's deployment secret sources, with a
+// separate purpose/key. Development continuation is process-local, as before.
+const developmentCommitmentKey = randomBytes(32);
+function collectionCommitmentKey(): Buffer {
+ const configured =
+ process.env.STORYBOARD_CONTEXT_REF_SECRET ||
+ process.env.WORKOS_COOKIE_PASSWORD ||
+ process.env.CONFORMANCE_JWT_SECRET;
+ return configured
+ ? scryptSync(configured, "adcp-collection-successful-create-v1", 32)
+ : developmentCommitmentKey;
+}
+
+function canonicalJson(value: unknown): string {
+ return JSON.stringify(value, (_key, child: unknown) => {
+ if (!child || typeof child !== "object" || Array.isArray(child))
+ return child;
+ return Object.fromEntries(
+ Object.entries(child).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
+ );
+ });
+}
+
+function collectionCommitment(
+ agentUrl: AgentUrl,
+ storyboard: Storyboard,
+ options: StoryboardRunOptions,
+ context: Record,
+): string {
+ const ids = [
+ "collection_media_buy_id",
+ "collection_package_id",
+ "collection_committed_product_id",
+ ];
+ const normalized = normalizeCollectionSelectors(
+ context.collection_committed_selectors,
+ );
+ if (
+ ids.some(
+ (key) => !Object.hasOwn(context, key) || typeof context[key] !== "string",
+ ) ||
+ !Object.hasOwn(context, "collection_committed_selectors") ||
+ !normalized.ok ||
+ normalized.value.pairs.length === 0 ||
+ normalized.value.pairs.some((pair) => pair.collection_id === null)
+ ) {
+ throw new Error(
+ "Standalone collection readback requires trusted prior buy, package, product and concrete committed-selector context",
+ );
+ }
+ const pairs = normalized.value.pairs.map((pair) => [
+ pair.publisher_domain,
+ pair.collection_id,
+ ]);
+ pairs.sort((a, b) =>
+ Buffer.compare(
+ Buffer.from(JSON.stringify(a)),
+ Buffer.from(JSON.stringify(b)),
+ ),
+ );
+ // This proves that this caller actually graded the exact create successfully.
+ // It grants no access: every subsequent invocation still prepares and uses
+ // the SDK's current credentials/authorization. No signer is public/exported.
+ return createHmac("sha256", collectionCommitmentKey())
+ .update("adcp-catalog-collection-successful-create-v1\0")
+ .update(
+ canonicalJson({
+ seller: agentUrl,
+ storyboard,
+ adcpVersion: options.adcpVersion ?? null,
+ account: storyboard.context?.sandbox_account,
+ ids: ids.map((key) => context[key]),
+ pairs,
+ }),
+ )
+ .digest("hex");
+}
+
+function assertCollectionReadHistory(
+ agentUrl: AgentUrl,
+ storyboard: Storyboard,
+ options: StoryboardRunOptions,
+) {
+ const context = options.context ?? {};
+ const expected = collectionCommitment(agentUrl, storyboard, options, context);
+ const proof = context[COLLECTION_COMMITMENT];
+ if (
+ typeof proof !== "string" ||
+ !/^[a-f0-9]{64}$/.test(proof) ||
+ !timingSafeEqual(Buffer.from(proof, "hex"), Buffer.from(expected, "hex"))
+ ) {
+ throw new Error(
+ "Standalone collection readback requires verified successful-create history for this exact seller, storyboard and committed selection",
+ );
+ }
+}
type AgentUrl = Parameters[0];
// Supplemental adapter evidence on standalone results; the SDK's native
@@ -40,15 +160,23 @@ type PreparationEvidence = {
fixture_resolutions?: PreparedStoryboardFixtures["resolutions"];
};
-function isCatalogReporting(storyboard: Storyboard): boolean {
- return (
- REPORTING_IDS.has(storyboard.id) &&
- storyboard.fixture_resolution?.pricing_options?.some((declaration) =>
- declaration.context_outputs?.some(
- (output) => output.key === REPORTING_PRICE_CAPTURE,
- ),
- ) === true
- );
+function catalogScope(storyboard: Storyboard) {
+ const collections = COLLECTION_IDS.has(storyboard.id);
+ if (!collections && !REPORTING_IDS.has(storyboard.id)) return undefined;
+ const priceCapture = collections
+ ? COLLECTION_PRICE_CAPTURE
+ : REPORTING_PRICE_CAPTURE;
+ return storyboard.fixture_resolution?.pricing_options?.some((declaration) =>
+ declaration.context_outputs?.some((output) => output.key === priceCapture),
+ ) === true
+ ? {
+ priceCapture,
+ budgetContext: collections
+ ? COLLECTION_BUDGET_CONTEXT
+ : REPORTING_BUDGET_CONTEXT,
+ collections,
+ }
+ : undefined;
}
// Hosted policy only, not a protocol product-size limit or aggregate budget.
@@ -57,7 +185,7 @@ export function withHostedReportingBudget(
storyboard: Storyboard,
options: StoryboardRunOptions,
): StoryboardRunOptions {
- if (!isCatalogReporting(storyboard)) return options;
+ if (!catalogScope(storyboard)) return options;
const requested = options.transport?.maxResponseBytes;
// SDK zero disables its cap; nonpositive/nonfinite values are not tighter.
const cap =
@@ -105,7 +233,7 @@ async function refuseUncallablePreparationAuthorizer(
);
const authored = phase?.steps.find((s) => s.id === stepId);
const detail =
- "Fresh reporting preparation cannot consult the supplied host authorizer before seller contact with SDK 16.3; see SDK #3389";
+ "Fresh catalog fixture preparation cannot consult the supplied host authorizer before seller contact with SDK 16.3; see SDK #3389";
const step: StoryboardStepResult = {
storyboard_id: storyboard.id,
step_id: authored?.id ?? "prepared_fixtures_authorization",
@@ -191,8 +319,38 @@ async function withReportingPreparation(
options: StoryboardRunOptions,
execute: (preparedOptions: StoryboardRunOptions) => Promise,
stepId?: string,
+ executeRootGate?: (options: StoryboardRunOptions) => Promise,
): Promise {
- if (!isCatalogReporting(storyboard)) return execute(options);
+ const scope = catalogScope(storyboard);
+ if (!scope) return execute(options);
+ const nativeExecute = execute;
+ // Every native fallback must obey the same precondition. Capability failures
+ // cannot turn a missing branch-safe comparison key into a successful read.
+ execute = async (executionOptions) => {
+ if (scope.collections && stepId === "read_selection") {
+ assertCollectionReadHistory(agentUrl, storyboard, executionOptions);
+ }
+ const result = await nativeExecute(executionOptions);
+ if (
+ scope.collections &&
+ storyboard.id.endsWith("_readback") &&
+ stepId === "create_selection"
+ ) {
+ const step = result as unknown as StoryboardStepResult;
+ const context = { ...step.context };
+ delete context[COLLECTION_COMMITMENT];
+ if (step.passed && !step.skipped) {
+ context[COLLECTION_COMMITMENT] = collectionCommitment(
+ agentUrl,
+ storyboard,
+ executionOptions,
+ context,
+ );
+ }
+ return { ...result, context };
+ }
+ return result;
+ };
options.signal?.throwIfAborted();
// Delegate invalid caller maps to the SDK's canonical denial BEFORE our
// capability discovery. Do not sanitize them or produce a substitute grade.
@@ -212,13 +370,16 @@ async function withReportingPreparation(
),
)
) {
- return execute(options);
+ // This route is a canonical SDK pre-contact metadata refusal, never an
+ // executable read fallback. Preserve its typed denial/private audit even
+ // when no successful-create history exists.
+ return nativeExecute(options);
}
// These entry points own one preparation for one authenticated invocation.
// Never silently discard a supplied reuse grant, or re-resolve it instead.
if (options.prepared_fixtures !== undefined) {
throw new Error(
- "Catalog reporting preparation must be owned by this invocation; use the SDK directly to reuse an existing authorized state.",
+ "Catalog fixture preparation must be owned by this invocation; use the SDK directly to reuse an existing authorized state.",
);
}
// The authored sandbox identity is trusted source, never a seller echo or
@@ -228,10 +389,14 @@ async function withReportingPreparation(
options.disable_sandbox === true ||
options.context?.sandbox === false
) {
- throw new Error("Catalog reporting requires the authored sandbox scope");
+ throw new Error(
+ "Catalog fixture preparation requires the authored sandbox scope",
+ );
}
if (options.agents || (Array.isArray(agentUrl) && agentUrl.length !== 1)) {
- throw new Error("Catalog reporting targets one seller per invocation");
+ throw new Error(
+ "Catalog fixture preparation targets one seller per invocation",
+ );
}
if (options.prepared_fixtures_authorizer !== undefined) {
return refuseUncallablePreparationAuthorizer(
@@ -251,7 +416,7 @@ async function withReportingPreparation(
`Step "${stepId}" not found in storyboard "${storyboard.id}". Available steps: ${storyboard.phases.flatMap((phase) => phase.steps.map((step) => step.id)).join(", ")}`,
);
}
- // Ignore the legacy first-product cache ONLY in these two scopes. All
+ // Ignore the legacy first-product cache ONLY in the declared catalog scopes. All
// identities/commercial terms come from the pinned graph. Otherwise the
// SDK's generic create enricher invents an auction bid from an unrelated
// cached CPM row, even for a selected fixed-price option.
@@ -317,7 +482,13 @@ async function withReportingPreparation(
) {
return execute(originalOptions);
}
- options = { ...options, profile };
+ // In collection scopes the observed inventory is also the native runner's
+ // effective inventory; an obsolete caller cache must not override this gate.
+ options = {
+ ...options,
+ profile,
+ ...(scope.collections && { agentTools: [...profile.tools] }),
+ };
const capabilities = profile.raw_capabilities as
Record | undefined;
const mediaBuy = capabilities?.media_buy as
@@ -334,32 +505,87 @@ async function withReportingPreparation(
!mediaBuy.buying_modes.includes("wholesale") ||
!storyboard.required_tools?.some((tool) => tools.has(tool))
) {
+ if (scope.collections && executeRootGate) {
+ // SDK16.3 standalone execution bypasses root applicability gates. Keep
+ // the observed profile stable and return the actual native full-run
+ // gate. Never rename an authored skip or relax a hosted failure grade.
+ return executeRootGate({
+ ...options,
+ agentTools: [...tools],
+ profile: { ...profile, tools: [...tools] },
+ });
+ }
return execute(options); // Let the unchanged SDK produce its canonical gate.
}
+ if (scope.collections && stepId === "read_selection") {
+ // Branch-safe field_equals_context intentionally passes when a comparand is
+ // absent. Standalone readback has no preceding create in this invocation,
+ // so require its trusted prior identities before preparing or dispatching.
+ assertCollectionReadHistory(agentUrl, storyboard, options);
+ }
const prepared = await prepareStoryboardFixtures(
agentUrl,
storyboard,
options,
);
options.signal?.throwIfAborted();
- let budget: number | undefined;
+ const derivedContext: Record = {};
if (prepared.disposition.status === "resolved") {
- const price = prepared.captures[REPORTING_PRICE_CAPTURE] as Record<
+ const price = prepared.captures[scope.priceCapture] as Record<
string,
unknown
>;
if (!price || typeof price !== "object")
- throw new Error(
- "Resolved reporting graph has no selected price snapshot",
- );
+ throw new Error("Resolved catalog graph has no selected price snapshot");
const minimum = price.min_spend_per_package;
if (
minimum !== undefined &&
(typeof minimum !== "number" || !Number.isFinite(minimum) || minimum < 0)
) {
- throw new Error("Selected reporting price has an invalid minimum spend");
+ throw new Error("Selected catalog price has an invalid minimum spend");
+ }
+ derivedContext[scope.budgetContext] = Math.max(
+ BASELINE_BUDGET,
+ minimum ?? 0,
+ );
+ if (scope.collections) {
+ // Product composition may legally repeat entries; selected wire arrays
+ // require unique entries. Project the SAME protected capture through the
+ // public normalizer without altering its immutable evidence snapshot.
+ const normalized = normalizeCollectionSelectors(
+ prepared.captures[COLLECTION_DECLARED_CAPTURE],
+ );
+ if (
+ !normalized.ok ||
+ normalized.value.pairs.some((pair) => pair.collection_id === null)
+ ) {
+ throw new Error(
+ "Resolved collection graph has no concrete selector set",
+ );
+ }
+ derivedContext[COLLECTION_FULL_CONTEXT] = normalized.value.pairs.map(
+ (pair) => ({
+ publisher_domain: pair.publisher_domain,
+ collection_ids: [pair.collection_id],
+ }),
+ );
+ const relation = prepared.captures[COLLECTION_RELATION_CAPTURE] as
+ { collection_publisher_domain?: unknown } | undefined;
+ if (relation !== undefined) {
+ if (typeof relation?.collection_publisher_domain !== "string") {
+ throw new Error(
+ "Resolved collection graph has no cross-publisher relation",
+ );
+ }
+ // Intentionally malformed buyer selection for the schema-invalid
+ // negative. Its domain comes from the retained relation, not index 0.
+ derivedContext[COLLECTION_BULK_CONTEXT] = [
+ {
+ publisher_domain: relation.collection_publisher_domain,
+ },
+ ];
+ }
}
- budget = Math.max(BASELINE_BUDGET, minimum ?? 0);
}
// The trusted caller authorized this invocation. The state is never shared
// or serialized; close its local grant when the invocation ends. Supplied
@@ -370,7 +596,7 @@ async function withReportingPreparation(
...options,
context: {
...options.context,
- ...(budget === undefined ? {} : { [REPORTING_BUDGET_CONTEXT]: budget }),
+ ...derivedContext,
},
...withPreparedFixtures(prepared, authorize),
// The producer ran before consumer authorization. Link its real records
@@ -447,5 +673,20 @@ export function runCatalogReportingStoryboardStep(
(preparedOptions) =>
runSdkStoryboardStep(agentUrl, storyboard, stepId, preparedOptions),
stepId,
+ async (gatedOptions) => {
+ const result = await runSdkStoryboard(agentUrl, storyboard, gatedOptions);
+ const steps = result.phases.flatMap((phase) => phase.steps);
+ if (
+ result.passed_count !== 0 ||
+ result.failed_count !== 0 ||
+ steps.length !== 1 ||
+ !steps[0].skipped
+ ) {
+ throw new Error(
+ "The native catalog applicability gate did not produce a single skipped gate",
+ );
+ }
+ return steps[0];
+ },
);
}
diff --git a/server/tests/unit/addie/catalog-reporting-coverage-presentation.test.ts b/server/tests/unit/addie/catalog-reporting-coverage-presentation.test.ts
index e2ca043ac36..02bf495be2a 100644
--- a/server/tests/unit/addie/catalog-reporting-coverage-presentation.test.ts
+++ b/server/tests/unit/addie/catalog-reporting-coverage-presentation.test.ts
@@ -57,4 +57,53 @@ describe('Addie reporting fixture coverage presentation', () => {
mocks.result.passed_count = 5;
expect(await render()).toContain('**Result:** PASSED');
});
+ it.each([
+ "collection_selection",
+ "collection_default_materialization",
+ "fixed_bundle_enforcement",
+ "fixed_bundle_restatement",
+ "owner_sold_channel_carriage",
+ "owner_sold_channel_subset",
+ "collection_selection_readback",
+ "collection_default_materialization_readback",
+ ])(
+ "presents %s catalog fixture gaps as untested without changing raw accounting",
+ async (id) => {
+ expect(await render(`media_buy_seller/${id}`)).toContain(
+ "NOT APPLICABLE — no qualifying product",
+ );
+ expect(mocks.result.passed_count).toBe(0);
+ expect(mocks.result.overall_passed).toBe(true);
+ },
+ );
+ it.each([
+ "collection_selection_readback",
+ "collection_default_materialization_readback",
+ ])(
+ "does not credit absent optional reader in %s as passed support",
+ async (id) => {
+ mocks.result.coverage_gaps = [];
+ mocks.result.skipped_count = 1;
+ mocks.result.phases = [
+ {
+ phase_id: "missing_tool",
+ steps: [
+ {
+ step_id: "missing_tool",
+ skipped: true,
+ passed: false,
+ skip_reason: "missing_tool",
+ },
+ ],
+ },
+ ];
+ expect(await render(`media_buy_seller/${id}`)).toContain(
+ "NOT APPLICABLE — get_media_buys is not advertised",
+ );
+ mocks.result.failed_count = 1;
+ expect(await render(`media_buy_seller/${id}`)).toContain(
+ "**Result:** FAILED",
+ );
+ },
+ );
});
diff --git a/specs/optional-product-collection-qualification.md b/specs/optional-product-collection-qualification.md
new file mode 100644
index 00000000000..0976233f51e
--- /dev/null
+++ b/specs/optional-product-collection-qualification.md
@@ -0,0 +1,163 @@
+# Catalog-qualified optional collection checks
+
+This is the implementation record for #8254's collection work. Reporting was
+separately corrected in [#8336](https://github.com/adcontextprotocol/adcp/pull/8336).
+It supersedes the unimplemented prerequisites in
+[#8259](https://github.com/adcontextprotocol/adcp/pull/8259); that proposal's old
+SDK15.9.5/pristine15.9.6 defect observations remain historical evidence at its
+original head, not qualification of current production behavior.
+
+## Existing authority and release scope
+
+[DR-0010](../governance/decisions/DR-0010-fixture-handles-and-resolution.md) defines
+fixture handles, scoped pricing identities, pinned whole-graph resolution,
+terminal discovery failures and `fixture_unsatisfied` coverage gaps. The merged
+[SDK foundation #3354](https://github.com/adcontextprotocol/adcp-client/pull/3354)
+and [authoring/output contract #8376](https://github.com/adcontextprotocol/adcp/pull/8376)
+supply the predicate, captures, pair-set comparison and authenticated preparation.
+The implementation uses the already installed public npm SDK16.3.0. No dependency,
+lockfile, installer, capability declaration or controller contract changes here.
+
+This is a protocol-package **patch** to existing compliance obligations on main,
+with new scenario IDs introduced in the upcoming 3.3 line. It does not ratify
+[future declarations #8333](https://github.com/adcontextprotocol/adcp/pull/8333),
+qualify SDK14/9, prepare a maintenance backport or authorize publication. The
+proposed DR-0028 specialism is not authority for these applicability rules.
+
+## Independent applicability scopes
+
+All IDs below have the `media_buy_seller/` prefix. Each resolves its own product
+and SAME-parent fixed-price CPM option; no missing companion suppresses another.
+
+| Scenario | Product predicate | Graded behavior |
+| --------------------------------------------- | ------------------------------------------ | -------------------------------------------------------------- |
+| `collection_selection` | selectable, 2 distinct pairs | Strict subset; undeclared ID and domain rejected |
+| `collection_default_materialization` | any, 1 pair | Default materializes the complete concrete set |
+| `fixed_bundle_enforcement` | fixed, 2 distinct pairs | Full set accepted; strict subset rejected |
+| `fixed_bundle_restatement` | fixed, 1 pair | Exact full-set restatement accepted |
+| `owner_sold_channel_carriage` | any, 1 pair, cross-publisher | Full set accepted; bulk-form and undeclared selectors rejected |
+| `owner_sold_channel_subset` | selectable, 2 total pairs, cross-publisher | Strict subset retains the external collection pair |
+| `collection_selection_readback` | selectable, 2 distinct pairs | Persisted subset, buy/package/product identity |
+| `collection_default_materialization_readback` | any, 1 pair | Persisted materialized default and identities |
+
+Fixed means `collection_targeting_allowed` is omitted or literally false; null
+is schema-invalid. One local and one external pair suffice for cross-publisher
+subset coverage. A one-pair default or fixed-restatement product never needs a
+two-pair companion. Cross-publisher is structural: the retained collection domain
+lies outside **all** singular publisher-property domains of the same product.
+It attests neither beneficial ownership, AOR, host authorization nor supply-path
+verification. The historical carriage filename is retained for continuity.
+
+The six core scenarios require creation, not an optional reader. Only the two
+read companions have the root `get_media_buys` tool gate. The gate is any-of by
+existing contract, so the root list contains only that reader. An advertised
+broken reader fails. An absent reader returns the actual native missing-tool
+gate; it does not turn an authored skip into a pass. The repository's standalone
+caller executes this same native root gate because SDK16.3's bare standalone
+entry does not apply enclosing storyboard gates. Observed profile tools are
+bound to the native `agentTools` inventory so a stale caller cache cannot bypass
+it. Evaluator disposition, required-phase floors and global skip rules are unchanged.
+
+## Selection, construction and evidence
+
+Product and price declarations use only `discover`, never seed fallback. Account
+setup remains in the existing sandbox controller contract. The discovery path
+exhausts pagination and validates pages before selecting by deterministic order;
+a later qualifying product wins over an earlier nonqualifying one. A complete
+valid no-match is an explicit `fixture_unsatisfied` gap, projected
+`not_applicable`: feature **not tested**, never a pass or proof of nonsupport.
+Fixture catalog transport, protocol, schema, malformed/repeated-cursor and capture
+errors remain failed. Native capability-discovery delegation has its separately
+disclosed [SDK #3392](https://github.com/adcontextprotocol/adcp-client/issues/3392) limit. Missing controller/account setup has its existing distinct disposition.
+
+The protected selected graph supplies `declared_selectors`, `strict_subset`, the
+two collision-checked `undeclared_*` selector arrays, structural relation and
+selected product ID. Negatives use the complete selected product, including the
+retained external relation when required. ID identity is the exact
+(domain, collection ID) pair: legal empty/Unicode IDs, grouping, order and catalog
+redundancy do not change comparison. The caller projects distinct canonical wire
+entries using the public normalizer; it never rewrites protected raw captures.
+An authorization-style domain-only selector is used only in the explicit
+schema-invalid negative, whose domain comes from the retained relation.
+
+Whole `snapshot: selected_entity` pricing preserves absent minimum, explicit0 and 5000. The caller constructs budget `max(1000, min_spend_per_package ?? 0)` from
+that SAME price; 1000 is buyer test intent, not a fabricated seller term. The
+request has no invented top-level currency or auction bid. Required schema,
+exact package cardinality, explicit product-ID echo and exact collection-set
+checks remain independent assertions. Unknown references require
+`REFERENCE_NOT_FOUND`, malformed bulk-form requires `INVALID_REQUEST`, and fixed
+bundle narrowing requires `UNSUPPORTED_FEATURE` under the existing schemas.
+
+Full runs prepare once, then consume the authenticated state with invocation-local
+lookup authority. Caller-supplied host authorizers still fail closed before HTTP
+until [SDK #3389](https://github.com/adcontextprotocol/adcp-client/issues/3389);
+the caller does not claim an uncallable hook ran. Denials stay typed exceptions
+with private audit records. The invocation grant is closed in `finally` and no
+portable or cross-request grant service is claimed.
+
+Read companions consume committed selectors and product ID only after create
+passes its protected-witness assertion in a full run. SDK16.3 publishes step
+captures before final validation, so failed-step context is not proof of a
+successful commitment and must not be reused as trusted standalone history.
+Reads compare that committed set rather than later catalog composition.
+The caller issues an integrity-only HMAC commitment after the actual standalone
+create passes. Its closed claims bind the exact authored storyboard, seller,
+AdCP version, authored sandbox account, buy/package/product identities and
+normalized concrete pair set. It uses a purpose-separated key derived from the
+existing hosted context deployment-secret sources, or a process-private random
+key for local development; no public signing API or durable grant store is added.
+JSON context round-trips retain the proof, but modified identities/selectors or a
+failed create cannot acquire one. This proof grants no seller access: fresh
+preparation and all SDK authorization still run on each invocation. Hosted
+opaque-reference handoff seals the pre-call continuation on failure/skips while
+retaining the failed result's raw captures, validations and ordinary response.
+Standalone reads require that successful-create proof and its buy,
+package, product and concrete committed-selector context before preparation and
+on **every** native discovery-error/malformed-profile fallback;
+missing keys are local invocation errors, not fabricated seller failures. This
+also prevents generic branch-safe absent-comparand behavior from certifying a
+missing identity. Standalone currently requalifies the catalog; retirement of a
+previously selected product is not handled as durable prepared-state reuse.
+
+## Executable qualification and bounded SDK gaps
+
+`npm run test:optional-product-collections` executes the actual eight YAMLs via
+the public SDK MCP transport and production caller, with controlled HTTP and
+seller-owned synthetic catalogs. It covers supporting/nonsupporting/independently
+supporting catalogs, omitted/false fixed flags, one/two-pair separation,
+later-page selection, same-parent commercial terms, optional minima, legal set
+variations, collision controls, invalid catalogs, broken echoes/identities,
+negative rejection codes, readback, root gates, private audit and zero-contact
+host-hook refusal. The fake seller uses a separate domain-qualified set oracle,
+so the production normalizer is not its own expected-answer implementation.
+Actual reference-CI rows must be inspected separately; a fixture gap is not
+native positive-path evidence.
+
+The following repaired-behavior probes are intentionally **not** passing CI
+regressions. Run them manually from the repository root; each fails naturally on
+SDK16.3.0. Their original failing assertions are retained, not changed into
+success-by-observing-a-defect:
+
+| Public probe | Actual SDK16.3 result and qualification limit |
+| ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `node specs/optional-product-collection-qualification/sdk3395-resource-status.cjs` | Explicit lifecycle control passes; omitting optional `media_buy_status` falsely fails later active read. [SDK #3395](https://github.com/adcontextprotocol/adcp-client/issues/3395). The optional field remains optional. |
+| `node specs/optional-product-collection-qualification/sdk3396-context-key.cjs` | Widened subset correctly fails, but failed output omits authored `context_key`. [SDK #3396](https://github.com/adcontextprotocol/adcp-client/issues/3396). The merged output MUST remains in force; this metadata path is unqualified. |
+| `node specs/optional-product-collection-qualification/sdk3397-feed-restart.cjs` | Feed changes v1→v2 between pages; resolver makes two page calls and resolves instead of restarting and making four after stabilization. [SDK #3397](https://github.com/adcontextprotocol/adcp-client/issues/3397). Mutable-feed restart remains unqualified. |
+
+The first split matrix was75pass/6fail: four assertions mistakenly classified a
+conforming feed-token change as seller failure, and two exposed the genuine
+context-key omission. Protocol review corrected the former using the existing
+get_products buyer-restart MUST; invalid `adcp_version` is now a separate true
+schema-failure case. The latter's unchanged normative assertion is preserved in
+the public probe. Initial minimized feed/status drafts with incorrect tool
+profile/raw output key were setup errors; corrected probes exercise actual wire
+behavior. These historical diagnostics are not final qualification counts.
+
+Only native wholesale sandbox, non-guaranteed fixed-price CPM support is tested.
+Guaranteed, auction-priced, brief-only and seeded-only support is not tested.
+Bare `comply()` and the scheduled heartbeat lack this caller's selector/budget
+projection ([SDK #3382](https://github.com/adcontextprotocol/adcp-client/issues/3382));
+no heterogeneous bundle-branding parity is claimed. Hosted cards cannot yet
+separate absent offerings from untested fixture slices
+([#8416](https://github.com/adcontextprotocol/adcp/issues/8416)). These disclosed
+limits do not waive broken selected-path failures or establish seller nonsupport.
diff --git a/specs/optional-product-collection-qualification/sdk3395-resource-status.cjs b/specs/optional-product-collection-qualification/sdk3395-resource-status.cjs
new file mode 100644
index 00000000000..9e12c746512
--- /dev/null
+++ b/specs/optional-product-collection-qualification/sdk3395-resource-status.cjs
@@ -0,0 +1,36 @@
+const assert = require('node:assert/strict');
+const {runStoryboard}=require('@adcp/sdk/testing');
+const tools=['get_adcp_capabilities','create_media_buy','get_media_buys'];
+const caps={status:'completed',adcp:{major_versions:[3],supported_versions:['3.2'],idempotency:{supported:false}},supported_protocols:['media_buy'],media_buy:{buying_modes:['wholesale']}};
+const account={brand:{domain:'acmeoutdoor.example'},operator:'pinnacle-agency.example',sandbox:true};
+const doc={id:'media_buy_seller/status_identity_probe',version:'1.0.0',title:'Task status is not resource lifecycle',category:'media_buy_seller',required_tools:['create_media_buy'],phases:[{id:'lifecycle',title:'Create then read',steps:[
+ {id:'create',title:'Create',task:'create_media_buy',stateful:true,schema_ref:'media-buy/create-media-buy-request.json',response_schema_ref:'media-buy/create-media-buy-response.json',sample_request:{brand:account.brand,account,idempotency_key:'status-probe-create',start_time:'asap',end_time:'2099-09-30T23:59:59Z',packages:[{product_id:'seller-product',pricing_option_id:'seller-price',budget:1000}]},context_outputs:[{key:'buy_id',path:'media_buy_id'}],validations:[{check:'response_schema',description:'Canonical create schema'}]},
+ {id:'read',title:'Read',task:'get_media_buys',stateful:true,schema_ref:'media-buy/get-media-buys-request.json',response_schema_ref:'media-buy/get-media-buys-response.json',sample_request:{account,media_buy_ids:['$context.buy_id']},validations:[{check:'response_schema',description:'Canonical read schema'}]},
+]}]};
+async function run(includeLifecycle){
+ const exchanges=[];
+ const trustedFetchFn=async(_url,init={})=>{
+ if(!init.body)return new Response(null,{status:init.method==='DELETE'?202:405});
+ const msg=JSON.parse(init.body);let result;
+ if(msg.method==='server/discover')return new Response(JSON.stringify({jsonrpc:'2.0',id:msg.id,error:{code:-32601,message:'Not supported'}}),{headers:{'content-type':'application/json'}});
+ if(msg.method==='initialize')result={protocolVersion:'2025-03-26',capabilities:{tools:{}},serverInfo:{name:'status-control',version:'1'}};
+ else if(msg.method.startsWith('notifications/'))return new Response(null,{status:202});
+ else if(msg.method==='tools/list')result={tools:tools.map(name=>({name,description:name,inputSchema:{type:'object',additionalProperties:true}}))};
+ else if(msg.method==='tools/call'){
+ const task=msg.params.name;let data;
+ if(task==='get_adcp_capabilities')data=caps;
+ else if(task==='create_media_buy')data={status:'completed',...(includeLifecycle?{media_buy_status:'active'}:{}),media_buy_id:'seller-buy',confirmed_at:'2026-10-23T00:00:00Z',revision:1,packages:[{package_id:'seller-package',product_id:'seller-product'}]};
+ else if(task==='get_media_buys')data={status:'completed',media_buys:[{media_buy_id:'seller-buy',status:'active',currency:'EUR',total_budget:1000,confirmed_at:'2026-10-23T00:00:00Z',revision:1,packages:[{package_id:'seller-package',product_id:'seller-product'}]}]};
+ else throw Error('Unexpected task '+task);
+ exchanges.push({task,request:msg.params.arguments,response:data});result={content:[{type:'text',text:JSON.stringify(data)}]};
+ }else throw Error('Unexpected method '+msg.method);
+ return new Response(JSON.stringify({jsonrpc:'2.0',id:msg.id,result}),{headers:{'content-type':'application/json'}});
+ };
+ const result=await runStoryboard('https://status-control.example/mcp',doc,{adcpVersion:'3.2.5',brand:account.brand,sandbox:true,timeout_ms:3000,profile:{tools,raw_capabilities:caps,supported_protocols:['media_buy']},transport:{trustedFetchFn}});
+ console.log(JSON.stringify({includeLifecycle,overall_passed:result.overall_passed,failed_count:result.failed_count,exchanges,steps:result.phases.flatMap(p=>p.steps.map(s=>({id:s.step_id,passed:s.passed,validations:s.validations})))}));
+ return result;
+}
+(async()=>{
+ assert.equal((await run(true)).overall_passed,true,'explicit lifecycle positive control');
+ assert.equal((await run(false)).overall_passed,true,'MAY omit lifecycle: task completed cannot establish terminal media-buy lifecycle');
+})().catch(error=>{console.error(error);process.exitCode=1;});
diff --git a/specs/optional-product-collection-qualification/sdk3396-context-key.cjs b/specs/optional-product-collection-qualification/sdk3396-context-key.cjs
new file mode 100644
index 00000000000..1e5b11f33e4
--- /dev/null
+++ b/specs/optional-product-collection-qualification/sdk3396-context-key.cjs
@@ -0,0 +1,38 @@
+const assert = require('node:assert/strict');
+const {runStoryboard}=require('@adcp/sdk/testing');
+const tools=['get_adcp_capabilities','create_media_buy','get_media_buys'];
+const caps={status:'completed',adcp:{major_versions:[3],supported_versions:['3.2'],idempotency:{supported:false}},supported_protocols:['media_buy'],media_buy:{buying_modes:['wholesale']}};
+const account={brand:{domain:'acmeoutdoor.example'},operator:'pinnacle-agency.example',sandbox:true};
+const doc={id:'media_buy_seller/collection_evidence_probe',version:'1.0.0',title:'Collection failure evidence',category:'media_buy_seller',required_tools:['create_media_buy'],context:{wanted:[{publisher_domain:'studio.example',collection_ids:['a']}]},phases:[{id:'selection',title:'Compare',steps:[
+ {id:'create',title:'Create',task:'create_media_buy',stateful:true,schema_ref:'media-buy/create-media-buy-request.json',response_schema_ref:'media-buy/create-media-buy-response.json',sample_request:{brand:account.brand,account,idempotency_key:'evidence-probe-create',start_time:'asap',end_time:'2099-09-30T23:59:59Z',packages:[{product_id:'seller-product',pricing_option_id:'seller-price',budget:1000,targeting_overlay:{collection_selection:{mode:'selected',collections:'$context.wanted'}}}]},validations:[{check:'response_schema',description:'Canonical create schema'},{id:'exact',check:'collection_set_equals_context',path:'packages[0].targeting_overlay.collection_selection.collections',context_key:'wanted',description:'Do not widen the requested set'}]},
+]}]};
+async function run(includeLifecycle){
+ const exchanges=[];
+ const trustedFetchFn=async(_url,init={})=>{
+ if(!init.body)return new Response(null,{status:init.method==='DELETE'?202:405});
+ const msg=JSON.parse(init.body);let result;
+ if(msg.method==='server/discover')return new Response(JSON.stringify({jsonrpc:'2.0',id:msg.id,error:{code:-32601,message:'Not supported'}}),{headers:{'content-type':'application/json'}});
+ if(msg.method==='initialize')result={protocolVersion:'2025-03-26',capabilities:{tools:{}},serverInfo:{name:'status-control',version:'1'}};
+ else if(msg.method.startsWith('notifications/'))return new Response(null,{status:202});
+ else if(msg.method==='tools/list')result={tools:tools.map(name=>({name,description:name,inputSchema:{type:'object',additionalProperties:true}}))};
+ else if(msg.method==='tools/call'){
+ const task=msg.params.name;let data;
+ if(task==='get_adcp_capabilities')data=caps;
+ else if(task==='create_media_buy')data={status:'completed',...(includeLifecycle?{media_buy_status:'active'}:{}),media_buy_id:'seller-buy',confirmed_at:'2026-10-23T00:00:00Z',revision:1,packages:[{package_id:'seller-package',product_id:'seller-product',targeting_overlay:{collection_selection:{mode:'selected',collections:[{publisher_domain:'studio.example',collection_ids:['a','b']}]}}}]};
+ else if(task==='get_media_buys')data={status:'completed',media_buys:[{media_buy_id:'seller-buy',status:'active',currency:'EUR',total_budget:1000,confirmed_at:'2026-10-23T00:00:00Z',revision:1,packages:[{package_id:'seller-package',product_id:'seller-product'}]}]};
+ else throw Error('Unexpected task '+task);
+ exchanges.push({task,request:msg.params.arguments,response:data});result={content:[{type:'text',text:JSON.stringify(data)}]};
+ }else throw Error('Unexpected method '+msg.method);
+ return new Response(JSON.stringify({jsonrpc:'2.0',id:msg.id,result}),{headers:{'content-type':'application/json'}});
+ };
+ const result=await runStoryboard('https://status-control.example/mcp',doc,{adcpVersion:'3.2.5',brand:account.brand,sandbox:true,timeout_ms:3000,profile:{tools,raw_capabilities:caps,supported_protocols:['media_buy']},transport:{trustedFetchFn}});
+ console.log(JSON.stringify({includeLifecycle,overall_passed:result.overall_passed,failed_count:result.failed_count,exchanges,steps:result.phases.flatMap(p=>p.steps.map(s=>({id:s.step_id,passed:s.passed,validations:s.validations})))}));
+ return result;
+}
+(async()=>{
+ const result=await run(true);
+ const failed=result.phases.flatMap(p=>p.steps.flatMap(s=>s.validations??[])).find(v=>v.check==='collection_set_equals_context'&&!v.passed);
+ assert.ok(failed,'broken seller widening must fail');
+ console.log(JSON.stringify({failed}));
+ assert.equal(failed.context_key,'wanted','existing runner-output-contract MUST echo authored context_key');
+})().catch(error=>{console.error(error);process.exitCode=1;});
diff --git a/specs/optional-product-collection-qualification/sdk3397-feed-restart.cjs b/specs/optional-product-collection-qualification/sdk3397-feed-restart.cjs
new file mode 100644
index 00000000000..86e467c9c2e
--- /dev/null
+++ b/specs/optional-product-collection-qualification/sdk3397-feed-restart.cjs
@@ -0,0 +1,36 @@
+const assert = require('node:assert/strict');
+const {prepareStoryboardFixtures:runStoryboard}=require('@adcp/sdk/testing');
+const tools=['get_adcp_capabilities','get_products','create_media_buy','get_media_buys'];
+const caps={status:'completed',adcp:{major_versions:[3],supported_versions:['3.2'],idempotency:{supported:false}},supported_protocols:['media_buy'],media_buy:{buying_modes:['wholesale']}};
+const account={brand:{domain:'acmeoutdoor.example'},operator:'pinnacle-agency.example',sandbox:true};
+const offer={product_id:'seller-product',name:'StreamHaus offer',description:'Sandbox',delivery_type:'non_guaranteed',channels:['olv'],publisher_properties:[{publisher_domain:'studio.example',selection_type:'all'}],format_options:[{format_option_id:'image',format_kind:'image',params:{width:300,height:250}}],reporting_capabilities:{available_reporting_frequencies:['daily'],expected_delay_minutes:0,timezone:'UTC',supports_webhooks:false,available_metrics:['impressions'],date_range_support:'date_range'},collections:[{publisher_domain:'studio.example',collection_ids:['a','b']}],collection_targeting_allowed:true,pricing_options:[{pricing_option_id:'seller-price',pricing_model:'cpm',currency:'EUR',fixed_price:17}]};
+const doc={id:'media_buy_seller/feed_restart_probe',version:'1.0.0',title:'Restart changed wholesale feed',category:'media_buy_seller',fixtures:{products:[{product_id:'catalog-product'}]},fixture_resolution:{products:[{handle:'catalog-product',strategies:['discover'],match:[{path:'',operator:'collection_selection_satisfies',value:{collection_targeting:'selectable',minimum_pairs:2}}]}]},phases:[{id:'consume',title:'Consume',steps:[]}]};
+async function run(includeLifecycle){
+ const exchanges=[];let version='v1';
+ const trustedFetchFn=async(_url,init={})=>{
+ if(!init.body)return new Response(null,{status:init.method==='DELETE'?202:405});
+ const msg=JSON.parse(init.body);let result;
+ if(msg.method==='server/discover')return new Response(JSON.stringify({jsonrpc:'2.0',id:msg.id,error:{code:-32601,message:'Not supported'}}),{headers:{'content-type':'application/json'}});
+ if(msg.method==='initialize')result={protocolVersion:'2025-03-26',capabilities:{tools:{}},serverInfo:{name:'status-control',version:'1'}};
+ else if(msg.method.startsWith('notifications/'))return new Response(null,{status:202});
+ else if(msg.method==='tools/list')result={tools:tools.map(name=>({name,description:name,inputSchema:{type:'object',additionalProperties:true}}))};
+ else if(msg.method==='tools/call'){
+ const task=msg.params.name;let data;
+ if(task==='get_adcp_capabilities')data=caps;
+ else if(task==='get_products'){const next=msg.params.arguments.pagination?.cursor;if(next)version='v2';data={status:'completed',products:next?[offer]:[],cache_scope:'account',wholesale_feed_version:version,pagination:{has_more:!next,...(!next?{cursor:'next'}:{})}};}
+ else if(task==='create_media_buy')data={status:'completed',...(includeLifecycle?{media_buy_status:'active'}:{}),media_buy_id:'seller-buy',confirmed_at:'2026-10-23T00:00:00Z',revision:1,packages:[{package_id:'seller-package',targeting_overlay:{collection_selection:{mode:'selected',collections:[{publisher_domain:'studio.example',collection_ids:['a','b']}]}}}]};
+ else if(task==='get_media_buys')data={status:'completed',media_buys:[{media_buy_id:'seller-buy',status:'active',currency:'EUR',total_budget:1000,confirmed_at:'2026-10-23T00:00:00Z',revision:1,packages:[{package_id:'seller-package'}]}]};
+ else throw Error('Unexpected task '+task);
+ exchanges.push({task,request:msg.params.arguments,response:data});result={content:[{type:'text',text:JSON.stringify(data)}]};
+ }else throw Error('Unexpected method '+msg.method);
+ return new Response(JSON.stringify({jsonrpc:'2.0',id:msg.id,result}),{headers:{'content-type':'application/json'}});
+ };
+ const result=await runStoryboard('https://status-control.example/mcp',doc,{adcpVersion:'3.2.5',brand:account.brand,sandbox:true,timeout_ms:3000,profile:{tools,raw_capabilities:caps,supported_protocols:['media_buy']},transport:{trustedFetchFn}});
+ console.log(JSON.stringify({disposition:result.disposition,exchanges}));
+ return {disposition:result.disposition,exchanges};
+}
+(async()=>{
+ const result=await run(true);
+ const pages=result.exchanges.filter(e=>e.task==='get_products');
+ assert.deepEqual(pages.map(e=>e.request.pagination?.cursor),[undefined,'next',undefined,'next'],'buyer MUST restart from cursor:null when feed version changes between pages');
+})().catch(error=>{console.error(error);process.exitCode=1;});
diff --git a/static/compliance/source/protocols/media-buy/index.yaml b/static/compliance/source/protocols/media-buy/index.yaml
index 98ef029d7ec..e51fd1ef626 100644
--- a/static/compliance/source/protocols/media-buy/index.yaml
+++ b/static/compliance/source/protocols/media-buy/index.yaml
@@ -69,6 +69,12 @@ requires_scenarios:
- media_buy_seller/budget_cap_timezone_override_rejected
- media_buy_seller/owner_sold_channel_carriage
- media_buy_seller/collection_selection
+ - media_buy_seller/collection_default_materialization
+ - media_buy_seller/collection_selection_readback
+ - media_buy_seller/collection_default_materialization_readback
+ - media_buy_seller/fixed_bundle_enforcement
+ - media_buy_seller/fixed_bundle_restatement
+ - media_buy_seller/owner_sold_channel_subset
- media_buy_seller/outcome_target_list_products_inert
narrative: |
@@ -394,11 +400,11 @@ phases:
context_outputs:
- name: product_format_kind
- path: 'products[0].format_options[0].format_kind'
+ path: "products[0].format_options[0].format_kind"
- name: product_id
- path: 'products[0].product_id'
+ path: "products[0].product_id"
- name: pricing_option_id
- path: 'products[0].pricing_options[0].pricing_option_id'
+ path: "products[0].pricing_options[0].pricing_option_id"
validations:
- check: response_schema
description: "Response matches get-products-response.json schema"
@@ -538,7 +544,7 @@ phases:
context_outputs:
- name: media_buy_id
- path: 'media_buy_id'
+ path: "media_buy_id"
validations:
- check: response_schema
description: "Response matches create-media-buy-response.json schema"
@@ -709,11 +715,11 @@ phases:
correlation_id: "media_buy_seller--get_products_brief"
context_outputs:
- name: product_format_kind
- path: 'products[0].format_options[0].format_kind'
+ path: "products[0].format_options[0].format_kind"
- name: product_id
- path: 'products[0].product_id'
+ path: "products[0].product_id"
- name: pricing_option_id
- path: 'products[0].pricing_options[0].pricing_option_id'
+ path: "products[0].pricing_options[0].pricing_option_id"
validations:
- check: response_schema
description: "Response matches get-products-response.json schema"
@@ -797,7 +803,7 @@ phases:
correlation_id: "media_buy_seller--create_media_buy"
context_outputs:
- name: media_buy_id
- path: 'media_buy_id'
+ path: "media_buy_id"
validations:
- check: response_schema
description: "Response matches create-media-buy-response.json schema"
diff --git a/static/compliance/source/protocols/media-buy/scenarios/collection_default_materialization.yaml b/static/compliance/source/protocols/media-buy/scenarios/collection_default_materialization.yaml
new file mode 100644
index 00000000000..66c41a7efea
--- /dev/null
+++ b/static/compliance/source/protocols/media-buy/scenarios/collection_default_materialization.yaml
@@ -0,0 +1,200 @@
+id: media_buy_seller/collection_default_materialization
+version: 1.0.0
+introduced_in: "3.3"
+title: Seller materializes a catalog product default as its complete concrete collection set
+category: media_buy_seller
+summary: Independently grades any collection support with at least 1 distinct domain-qualified pair.
+track: media_buy
+requires:
+ - controller
+required_tools:
+ - create_media_buy
+requires_capability:
+ path: media_buy.buying_modes
+ contains: wholesale
+narrative: >-
+ Applicability comes only from fully paginated, schema-valid wholesale sandbox discovery. Select
+ one seller-owned non-guaranteed product and a fixed-price CPM option from that SAME product. No
+ collection product or price is seeded. A completed valid no-match is fixture_unsatisfied: feature
+ not tested, not a pass or proof of nonsupport. Discovery, pagination, schema, capture and
+ selected-path failures remain failures.
+
+
+ This scope resolves independently of the other collection scopes. One-pair default and
+ fixed-restatement coverage do not require a two-pair subset fixture. Fixed targeting accepts
+ either literal false or omission of collection_targeting_allowed. Pair identity includes both
+ publisher_domain and collection_id; grouping, order and legal catalog redundancy do not change the
+ set.
+
+
+ This core scope grades create and rejection assertions independently of get_media_buys. Separate
+ readback companions cover persisted subset/default selection under their own advertised reader
+ gate. Required arrays, exact buy/package identities and concrete echoes remain graded.
+ Seeded-only, brief-only, guaranteed and auction-priced collection support is outside this catalog
+ slice. A controller without seed_account yields fixture_seed_unsupported; an absent controller
+ yields missing_test_controller.
+agent:
+ interaction_model: media_buy_seller
+ capabilities:
+ - sells_media
+ examples:
+ - A seller offering native collection-bearing sandbox products
+caller:
+ role: buyer_agent
+ example: Pinnacle Agency (buyer)
+prerequisites:
+ description:
+ Establish one isolated sandbox account, then discover the native product and its scoped
+ price. The repository catalog fixture caller prepares this graph once for full or standalone
+ execution, derives a valid budget from its whole selected price snapshot, and projects canonical
+ full selectors from the protected declaration. Null caller-derived context defaults are
+ unprepared sentinels, never fabricated selectors. Direct SDK callers must perform the same
+ preparation and projection before consuming this storyboard.
+ test_kit: test-kits/acme-outdoor.yaml
+ controller_seeding: true
+fixtures:
+ accounts:
+ - account_id: sandbox_collection_default_materialization
+ fixture:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_collection_default_-2af6e9ef
+ sandbox: true
+ billing: operator
+ status: active
+ products:
+ - product_id: catalog_collection_product
+ pricing_options:
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+fixture_resolution:
+ products:
+ - handle: catalog_collection_product
+ strategies:
+ - discover
+ match:
+ - path: ""
+ operator: collection_selection_satisfies
+ value:
+ collection_targeting: any
+ minimum_pairs: 1
+ - path: /delivery_type
+ operator: equals
+ value: non_guaranteed
+ - path: /pricing_options
+ operator: any_match
+ where:
+ - path: /pricing_model
+ operator: equals
+ value: cpm
+ - path: /fixed_price
+ operator: present
+ context_outputs:
+ - key: __adcp_fixture_collection_declared
+ witness: declared_selectors
+ - key: __adcp_fixture_collection_product_id
+ path: product_id
+ pricing_options:
+ - handle: catalog_collection_fixed_cpm
+ product_handle: catalog_collection_product
+ strategies:
+ - discover
+ match:
+ - path: /pricing_model
+ operator: equals
+ value: cpm
+ - path: /fixed_price
+ operator: present
+ context_outputs:
+ - key: __adcp_fixture_collection_price
+ snapshot: selected_entity
+context:
+ catalog_collection_budget: 1000
+ catalog_collection_full_selectors: null
+ brand:
+ domain: acmeoutdoor.example
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_collection_default_-2af6e9ef
+ sandbox: true
+ sandbox_account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_collection_default_-2af6e9ef
+ sandbox: true
+ sandbox_brand:
+ domain: acmeoutdoor.example
+phases:
+ - id: commit_selection
+ title: Grade the independently selected collection product
+ steps:
+ - id: create_selection
+ title: Purchase the complete default bundle
+ task: create_media_buy
+ schema_ref: media-buy/create-media-buy-request.json
+ response_schema_ref: media-buy/create-media-buy-response.json
+ doc_ref: /media-buy/task-reference/create_media_buy
+ stateful: true
+ sample_request:
+ brand: $context.sandbox_brand
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_collection_default_-2af6e9ef
+ sandbox: true
+ idempotency_key: $generate:uuid_v4#collection_default_materialization_create_selection
+ start_time: asap
+ end_time: "2099-09-30T23:59:59Z"
+ packages:
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+ budget: $context.catalog_collection_budget
+ targeting_overlay:
+ collection_selection:
+ mode: default
+ expected:
+ Accept the selection on the discovered product. Echo exactly the committed concrete
+ domain-qualified set in selected mode; default materializes the complete catalog
+ composition.
+ validations:
+ - check: response_schema
+ description: The response matches the canonical create-media-buy schema
+ - check: field_present
+ path: media_buy_id
+ description: The seller assigns the committed buy identity
+ - check: field_equals_context
+ path: packages[0].product_id
+ context_key: __adcp_fixture_collection_product_id
+ description: The response echoes the explicit SAME selected product identity
+ - check: array_length
+ path: packages
+ value: 1
+ description: The one-package request produces exactly one committed package
+ - check: field_value
+ path: packages[0].targeting_overlay.collection_selection.mode
+ value: selected
+ description: Package readback materializes concrete selected-form collection selectors
+ - check: collection_set_equals_context
+ path: packages[0].targeting_overlay.collection_selection.collections
+ context_key: __adcp_fixture_collection_declared
+ description:
+ The committed set exactly matches the selected product witness, preserving publisher
+ namespaces without widening
+ context_outputs:
+ - name: collection_media_buy_id
+ path: media_buy_id
+ - name: collection_package_id
+ path: packages[0].package_id
+ - name: collection_committed_product_id
+ path: packages[0].product_id
+ - name: collection_committed_selectors
+ path: packages[0].targeting_overlay.collection_selection.collections
diff --git a/static/compliance/source/protocols/media-buy/scenarios/collection_default_materialization_readback.yaml b/static/compliance/source/protocols/media-buy/scenarios/collection_default_materialization_readback.yaml
new file mode 100644
index 00000000000..af2a8e4e3fa
--- /dev/null
+++ b/static/compliance/source/protocols/media-buy/scenarios/collection_default_materialization_readback.yaml
@@ -0,0 +1,266 @@
+id: media_buy_seller/collection_default_materialization_readback
+version: 1.0.0
+introduced_in: "3.3"
+title: Seller persists the materialized default catalog collection set
+category: media_buy_seller
+summary: Independently catalog-qualifies persisted collection readback when get_media_buys is advertised.
+track: media_buy
+requires:
+ - controller
+required_tools:
+ - get_media_buys
+requires_capability:
+ path: media_buy.buying_modes
+ contains: wholesale
+narrative: >-
+ Applicability comes only from fully paginated, schema-valid wholesale sandbox discovery. Select
+ one seller-owned non-guaranteed product and a fixed-price CPM option from that SAME product. No
+ collection product or price is seeded. A completed valid no-match is fixture_unsatisfied: feature
+ not tested, not a pass or proof of nonsupport. Discovery, pagination, schema, capture and
+ selected-path failures remain failures.
+
+
+ This scope resolves independently of the other collection scopes. One-pair default and
+ fixed-restatement coverage do not require a two-pair subset fixture. Fixed targeting accepts
+ either literal false or omission of collection_targeting_allowed. Pair identity includes both
+ publisher_domain and collection_id; grouping, order and legal catalog redundancy do not change the
+ set.
+
+
+ This companion grades creation followed by persisted readback under its own advertised
+ get_media_buys root gate; the six core scopes remain independent of this optional reader. Required arrays, exact buy/package identities and concrete echoes
+ remain graded.
+ Seeded-only, brief-only, guaranteed and auction-priced collection support is outside this catalog
+ slice. A controller without seed_account yields fixture_seed_unsupported; an absent controller
+ yields missing_test_controller.
+
+
+ This readback companion requires only get_media_buys in its root tool gate. A missing reader is a
+ genuine native storyboard-level coverage gate, not a relabeled authored failure. An advertised
+ reader must preserve the committed set. Callers of individual steps must honor the same root gate;
+ the repository caller returns the unchanged native root-gate step before preparation when the
+ reader is absent. Standalone reads need trusted prior buy, package and committed-selector context;
+ they requalify the current native catalog and do not provide durable cross-request prepared-state
+ authority.
+agent:
+ interaction_model: media_buy_seller
+ capabilities:
+ - sells_media
+ examples:
+ - A seller offering native collection-bearing sandbox products
+caller:
+ role: buyer_agent
+ example: Pinnacle Agency (buyer)
+prerequisites:
+ description:
+ Establish one isolated sandbox account, then discover the native product and its scoped
+ price. The repository catalog fixture caller prepares this graph once for full or standalone
+ execution, derives a valid budget from its whole selected price snapshot, and projects canonical
+ full selectors from the protected declaration. Null caller-derived context defaults are
+ unprepared sentinels, never fabricated selectors. Direct SDK callers must perform the same
+ preparation and projection before consuming this storyboard.
+ test_kit: test-kits/acme-outdoor.yaml
+ controller_seeding: true
+fixtures:
+ accounts:
+ - account_id: sandbox_collection_default_materialization_readback
+ fixture:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_collection_default_-c806cfbc
+ sandbox: true
+ billing: operator
+ status: active
+ products:
+ - product_id: catalog_collection_product
+ pricing_options:
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+fixture_resolution:
+ products:
+ - handle: catalog_collection_product
+ strategies:
+ - discover
+ match:
+ - path: ""
+ operator: collection_selection_satisfies
+ value:
+ collection_targeting: any
+ minimum_pairs: 1
+ - path: /delivery_type
+ operator: equals
+ value: non_guaranteed
+ - path: /pricing_options
+ operator: any_match
+ where:
+ - path: /pricing_model
+ operator: equals
+ value: cpm
+ - path: /fixed_price
+ operator: present
+ context_outputs:
+ - key: __adcp_fixture_collection_declared
+ witness: declared_selectors
+ - key: __adcp_fixture_collection_product_id
+ path: product_id
+ pricing_options:
+ - handle: catalog_collection_fixed_cpm
+ product_handle: catalog_collection_product
+ strategies:
+ - discover
+ match:
+ - path: /pricing_model
+ operator: equals
+ value: cpm
+ - path: /fixed_price
+ operator: present
+ context_outputs:
+ - key: __adcp_fixture_collection_price
+ snapshot: selected_entity
+context:
+ catalog_collection_budget: 1000
+ catalog_collection_full_selectors: null
+ brand:
+ domain: acmeoutdoor.example
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_collection_default_-c806cfbc
+ sandbox: true
+ sandbox_account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_collection_default_-c806cfbc
+ sandbox: true
+ sandbox_brand:
+ domain: acmeoutdoor.example
+phases:
+ - id: commit_selection
+ title: Commit and validate the selection before readback
+ steps:
+ - id: create_selection
+ title: Purchase the complete default bundle
+ task: create_media_buy
+ schema_ref: media-buy/create-media-buy-request.json
+ response_schema_ref: media-buy/create-media-buy-response.json
+ doc_ref: /media-buy/task-reference/create_media_buy
+ stateful: true
+ sample_request:
+ brand: $context.sandbox_brand
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_collection_default_-c806cfbc
+ sandbox: true
+ idempotency_key: $generate:uuid_v4#collection_default_materialization_readback_create_selection
+ start_time: asap
+ end_time: "2099-09-30T23:59:59Z"
+ packages:
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+ budget: $context.catalog_collection_budget
+ targeting_overlay:
+ collection_selection:
+ mode: default
+ expected:
+ Accept the selection on the discovered product. Echo exactly the committed concrete
+ domain-qualified set in selected mode; default materializes the complete catalog
+ composition.
+ validations:
+ - check: response_schema
+ description: The response matches the canonical create-media-buy schema
+ - check: field_present
+ path: media_buy_id
+ description: The seller assigns the committed buy identity
+ - check: field_equals_context
+ path: packages[0].product_id
+ context_key: __adcp_fixture_collection_product_id
+ description: The response echoes the explicit SAME selected product identity
+ - check: array_length
+ path: packages
+ value: 1
+ description: The one-package request produces exactly one committed package
+ - check: field_value
+ path: packages[0].targeting_overlay.collection_selection.mode
+ value: selected
+ description: Package readback materializes concrete selected-form collection selectors
+ - check: collection_set_equals_context
+ path: packages[0].targeting_overlay.collection_selection.collections
+ context_key: __adcp_fixture_collection_declared
+ description:
+ The committed set exactly matches the selected product witness, preserving publisher
+ namespaces without widening
+ context_outputs:
+ - name: collection_media_buy_id
+ path: media_buy_id
+ - name: collection_package_id
+ path: packages[0].package_id
+ - name: collection_committed_product_id
+ path: packages[0].product_id
+ - name: collection_committed_selectors
+ path: packages[0].targeting_overlay.collection_selection.collections
+ - id: persisted_readback
+ title: Grade the persisted committed set
+ steps:
+ - id: read_selection
+ title: Read the persisted exact collection selection when the seller offers readback
+ task: get_media_buys
+ requires_tool: get_media_buys
+ schema_ref: media-buy/get-media-buys-request.json
+ response_schema_ref: media-buy/get-media-buys-response.json
+ doc_ref: /media-buy/task-reference/get_media_buys
+ stateful: true
+ sample_request:
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_collection_default_-c806cfbc
+ sandbox: true
+ media_buy_ids:
+ - $context.collection_media_buy_id
+ expected:
+ Return exactly the requested buy and its committed package. Preserve the complete
+ domain-qualified collection set; an advertised broken readback remains a failure.
+ validations:
+ - check: response_schema
+ description: Readback matches the canonical get-media-buys schema
+ - check: array_length
+ path: media_buys
+ value: 1
+ description: Only the requested buy is returned
+ - check: field_equals_context
+ path: media_buys[0].media_buy_id
+ context_key: collection_media_buy_id
+ description: Readback belongs to the actual committed buy
+ - check: field_equals_context
+ path: media_buys[0].packages[0].product_id
+ context_key: collection_committed_product_id
+ description: Readback echoes the explicit committed product identity
+ - check: array_length
+ path: media_buys[0].packages
+ value: 1
+ description: The buy preserves exactly its one committed package
+ - check: field_equals_context
+ path: media_buys[0].packages[0].package_id
+ context_key: collection_package_id
+ description: Readback belongs to the actual committed package
+ - check: field_value
+ path: media_buys[0].packages[0].targeting_overlay.collection_selection.mode
+ value: selected
+ description: Readback contains concrete selected-form selectors, including materialized defaults
+ - check: collection_set_equals_context
+ path: media_buys[0].packages[0].targeting_overlay.collection_selection.collections
+ context_key: collection_committed_selectors
+ description:
+ Persisted selectors equal the committed set from the preceding successful create; a failed
+ create blocks full-run readback, and grouping or ordering may vary
diff --git a/static/compliance/source/protocols/media-buy/scenarios/collection_selection.yaml b/static/compliance/source/protocols/media-buy/scenarios/collection_selection.yaml
index 7c7b247264b..020717e137c 100644
--- a/static/compliance/source/protocols/media-buy/scenarios/collection_selection.yaml
+++ b/static/compliance/source/protocols/media-buy/scenarios/collection_selection.yaml
@@ -1,474 +1,285 @@
id: media_buy_seller/collection_selection
-version: "1.0.0"
+version: 1.0.1
introduced_in: "3.2"
-title: "Buyer commits collection selection and the seller echoes it as concrete selectors on readback"
+title: Seller accepts a catalog collection subset and preserves its exact selectors
category: media_buy_seller
-summary: "Verifies mode: selected accepts an explicit domain-qualified subset, mode: default materializes the product's full bundle on readback, package readback always echoes committed collection_selection as concrete selectors, and a fixed (non-selectable) bundle rejects partial selection while accepting an exact restatement."
+summary: Independently grades selectable collection support with at least 2 distinct domain-qualified pairs.
track: media_buy
requires:
- controller
-
required_tools:
- - get_products
- create_media_buy
- - get_media_buys
- - comply_test_controller
+requires_capability:
+ path: media_buy.buying_modes
+ contains: wholesale
+narrative: >-
+ Applicability comes only from fully paginated, schema-valid wholesale sandbox discovery. Select
+ one seller-owned non-guaranteed product and a fixed-price CPM option from that SAME product. No
+ collection product or price is seeded. A completed valid no-match is fixture_unsatisfied: feature
+ not tested, not a pass or proof of nonsupport. Discovery, pagination, schema, capture and
+ selected-path failures remain failures.
+
-narrative: |
- Products with multiple collections default to bundles; sellers can set
- collection_targeting_allowed: true to let buyers target a subset, the same
- pattern as property_targeting_allowed for properties
- (collections-and-installments.mdx, "Collection targeting"). Selection
- travels in targeting_overlay.collection_selection, mirroring
- placement_selection: mode: "selected" names the complete required set as
- domain-qualified selectors with explicit collection_ids, and mode:
- "default" accepts the product's full bundle.
+ This scope resolves independently of the other collection scopes. One-pair default and
+ fixed-restatement coverage do not require a two-pair subset fixture. Fixed targeting accepts
+ either literal false or omission of collection_targeting_allowed. Pair identity includes both
+ publisher_domain and collection_id; grouping, order and legal catalog redundancy do not change the
+ set.
- On package readback, collection_selection is the audit surface: sellers
- MUST echo the committed collection selection there as concrete selectors —
- including the materialized result of any collection_list composition — so
- a buyer can verify what content programming a package runs against without
- re-resolving mutable lists or re-fetching the product catalog
- (collections-and-installments.mdx, "Collection targeting"; core/package.json
- targeting_overlay). Because the schema's mode: "default" branch carries no
- collections field, a default-mode purchase's readback necessarily
- materializes as concrete "selected"-shaped selectors naming the product's
- full bundle.
+ This core scope grades create and rejection assertions independently of get_media_buys. Separate
+ readback companions cover persisted subset/default selection under their own advertised reader
+ gate. Required arrays, exact buy/package identities and concrete echoes remain graded.
+ Seeded-only, brief-only, guaranteed and auction-priced collection support is outside this catalog
+ slice. A controller without seed_account yields fixture_seed_unsupported; an absent controller
+ yields missing_test_controller.
agent:
interaction_model: media_buy_seller
capabilities:
- sells_media
examples:
- - "A CTV seller offering a multi-channel programmed-audio/video bundle"
-
+ - A seller offering native collection-bearing sandbox products
caller:
role: buyer_agent
- example: "Pinnacle Agency (buyer)"
-
+ example: Pinnacle Agency (buyer)
prerequisites:
- description: |
- The caller needs a brand identity for product discovery and buy creation.
- The controller seeds a selectable two-collection channel bundle
- (collection_targeting_allowed: true) and a companion fixed bundle of the
- same two collections (collection_targeting_allowed: false, the default)
- to exercise both the selectable and fixed-bundle behaviors.
- test_kit: "test-kits/acme-outdoor.yaml"
+ description:
+ Establish one isolated sandbox account, then discover the native product and its scoped
+ price. The repository catalog fixture caller prepares this graph once for full or standalone
+ execution, derives a valid budget from its whole selected price snapshot, and projects canonical
+ full selectors from the protected declaration. Null caller-derived context defaults are
+ unprepared sentinels, never fabricated selectors. Direct SDK callers must perform the same
+ preparation and projection before consuming this storyboard.
+ test_kit: test-kits/acme-outdoor.yaml
controller_seeding: true
-
fixtures:
accounts:
- - account_id: "acct_acmeoutdoor_example_sandbox_8852d176d548"
+ - account_id: sandbox_collection_selection
fixture:
brand:
- domain: "acmeoutdoor.example"
- operator: "pinnacle-agency.example"
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
operator_unit:
- id: "compliance-media_buy_seller_collection_selectio-7e6aac4c"
- billing: "operator"
- status: "active"
+ id: compliance-media_buy_seller_collection_selectio-7e6aac4c
sandbox: true
+ billing: operator
+ status: active
products:
- - product_id: "retro_bundle_ctv"
- delivery_type: "non_guaranteed"
- channels: ["ctv"]
- format_options:
- - format_option_id: "video_30s"
- format_kind: "video_hosted"
- params:
- duration_ms_exact: 30000
- publisher_properties:
- - publisher_domain: "hoststream.example"
- selection_type: "by_id"
- property_ids: ["hoststream_ctv"]
- collections:
- - publisher_domain: "channel-owner.example"
- collection_ids: ["retro_news", "vintage_static"]
- collection_targeting_allowed: true
- placements:
- - kind: "seller_inline"
- seller_agent:
- agent_url: "https://sales.channel-owner.example"
- publisher_domain: "channel-owner.example"
- placement_id: "owner_avails"
- name: "Channel-owner avails"
- mode: "included"
- - product_id: "retro_bundle_ctv_fixed"
- delivery_type: "non_guaranteed"
- channels: ["ctv"]
- format_options:
- - format_option_id: "video_30s"
- format_kind: "video_hosted"
- params:
- duration_ms_exact: 30000
- publisher_properties:
- - publisher_domain: "hoststream.example"
- selection_type: "by_id"
- property_ids: ["hoststream_ctv"]
- collections:
- - publisher_domain: "channel-owner.example"
- collection_ids: ["retro_news", "vintage_static"]
- placements:
- - kind: "seller_inline"
- seller_agent:
- agent_url: "https://sales.channel-owner.example"
- publisher_domain: "channel-owner.example"
- placement_id: "owner_avails"
- name: "Channel-owner avails"
- mode: "included"
+ - product_id: catalog_collection_product
pricing_options:
- - product_id: "retro_bundle_ctv"
- pricing_option_id: "retro_bundle_cpm"
- pricing_model: "cpm"
- currency: "USD"
- floor_price: 22.0
- - product_id: "retro_bundle_ctv_fixed"
- pricing_option_id: "retro_bundle_fixed_cpm"
- pricing_model: "cpm"
- currency: "USD"
- floor_price: 22.0
-
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+fixture_resolution:
+ products:
+ - handle: catalog_collection_product
+ strategies:
+ - discover
+ match:
+ - path: ""
+ operator: collection_selection_satisfies
+ value:
+ collection_targeting: selectable
+ minimum_pairs: 2
+ - path: /delivery_type
+ operator: equals
+ value: non_guaranteed
+ - path: /pricing_options
+ operator: any_match
+ where:
+ - path: /pricing_model
+ operator: equals
+ value: cpm
+ - path: /fixed_price
+ operator: present
+ context_outputs:
+ - key: __adcp_fixture_collection_declared
+ witness: declared_selectors
+ - key: __adcp_fixture_collection_product_id
+ path: product_id
+ - key: __adcp_fixture_collection_subset
+ witness: strict_subset
+ - key: __adcp_fixture_collection_unknown_id
+ witness: undeclared_collection_id_selectors
+ - key: __adcp_fixture_collection_unknown_domain
+ witness: undeclared_publisher_domain_selectors
+ pricing_options:
+ - handle: catalog_collection_fixed_cpm
+ product_handle: catalog_collection_product
+ strategies:
+ - discover
+ match:
+ - path: /pricing_model
+ operator: equals
+ value: cpm
+ - path: /fixed_price
+ operator: present
+ context_outputs:
+ - key: __adcp_fixture_collection_price
+ snapshot: selected_entity
+context:
+ catalog_collection_budget: 1000
+ catalog_collection_full_selectors: null
+ brand:
+ domain: acmeoutdoor.example
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_collection_selectio-7e6aac4c
+ sandbox: true
+ sandbox_account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_collection_selectio-7e6aac4c
+ sandbox: true
+ sandbox_brand:
+ domain: acmeoutdoor.example
phases:
- - id: selected_subset_lifecycle
- title: "Purchase and read back an explicit collection subset"
+ - id: commit_selection
+ title: Grade the independently selected collection product
steps:
- - id: get_products_retro_bundle
- title: "Discover the selectable channel bundle"
- task: get_products
- schema_ref: "media-buy/get-products-request.json"
- response_schema_ref: "media-buy/get-products-response.json"
- doc_ref: "/media-buy/product-discovery/collections-and-installments"
- stateful: true
- sample_request:
- buying_mode: "brief"
- brief: "Pre-roll across the HostStream channel bundle: Acme Retro News and Vintage Static"
- account:
- brand:
- domain: "acmeoutdoor.example"
- operator: "pinnacle-agency.example"
- operator_unit:
- id: "compliance-media_buy_seller_collection_selectio-7e6aac4c"
- sandbox: true
- filters:
- channels: ["ctv"]
- context:
- correlation_id: "collection_selection--discovery"
- expected: |
- Return the selectable two-collection bundle product, declaring
- collection_targeting_allowed: true.
- context_outputs:
- - key: "bundle_product_id"
- path: "products[0].product_id"
- - key: "bundle_pricing_option_id"
- path: "products[0].pricing_options[0].pricing_option_id"
- validations:
- - check: response_schema
- description: "Response matches get-products-response.json"
- - check: field_value
- path: "products[0].collection_targeting_allowed"
- value: true
- description: "Product declares collection_targeting_allowed, the gate for targeting_overlay.collection_selection (core/product.json collection_targeting_allowed)"
- - check: array_length
- path: "products[0].collections[0].collection_ids"
- value: 2
- description: "Product's full bundle has two collections, so a selected subset is a real narrowing"
-
- - id: create_media_buy_selected_subset
- title: "Buy an explicit domain-qualified subset of the bundle"
+ - id: create_selection
+ title: Purchase the catalog-qualified concrete collection set
task: create_media_buy
- schema_ref: "media-buy/create-media-buy-request.json"
- response_schema_ref: "media-buy/create-media-buy-response.json"
- doc_ref: "/media-buy/task-reference/create_media_buy"
+ schema_ref: media-buy/create-media-buy-request.json
+ response_schema_ref: media-buy/create-media-buy-response.json
+ doc_ref: /media-buy/task-reference/create_media_buy
stateful: true
sample_request:
- brand:
- domain: "acmeoutdoor.example"
+ brand: $context.sandbox_brand
account:
brand:
- domain: "acmeoutdoor.example"
- operator: "pinnacle-agency.example"
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
operator_unit:
- id: "compliance-media_buy_seller_collection_selectio-7e6aac4c"
+ id: compliance-media_buy_seller_collection_selectio-7e6aac4c
sandbox: true
- idempotency_key: "$generate:uuid_v4#collection_selection_selected_subset_create"
- start_time: "asap"
+ idempotency_key: $generate:uuid_v4#collection_selection_create_selection
+ start_time: asap
end_time: "2099-09-30T23:59:59Z"
packages:
- - product_id: "$context.bundle_product_id"
- pricing_option_id: "$context.bundle_pricing_option_id"
- bid_price: 22.0
- budget: 5000
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+ budget: $context.catalog_collection_budget
targeting_overlay:
collection_selection:
- mode: "selected"
- collections:
- - publisher_domain: "channel-owner.example"
- collection_ids: ["retro_news"]
- expected: |
- Accept the subset because collection_targeting_allowed: true
- permits buyers to select fewer than the product's full bundle. The
- response package echoes the committed selection as a concrete,
- domain-qualified selector naming exactly the requested collection.
- context_outputs:
- - key: "selected_media_buy_id"
- path: "media_buy_id"
- - key: "selected_package_id"
- path: "packages[0].package_id"
- validations:
- - check: response_schema
- description: "Response matches create-media-buy-response.json"
- - check: field_value
- path: "packages[0].targeting_overlay.collection_selection.mode"
- value: "selected"
- description: "Sellers MUST echo collection selection on package readback (core/package.json targeting_overlay)"
- - check: field_value
- path: "packages[0].targeting_overlay.collection_selection.collections[0].publisher_domain"
- value: "channel-owner.example"
- description: "Echoed selector is domain-qualified (core/collection-selector.json requires publisher_domain)"
- - check: field_value
- path: "packages[0].targeting_overlay.collection_selection.collections[0].collection_ids[0]"
- value: "retro_news"
- description: "Echoed selector names the committed collection as a concrete selector (collections-and-installments.mdx, Collection targeting)"
- - check: field_absent
- path: "packages[0].targeting_overlay.collection_selection.collections[0].collection_ids[1]"
- description: "Committed subset has exactly one collection_id; the seller must not silently widen it"
- - check: field_absent
- path: "packages[0].targeting_overlay.collection_selection.collections[1]"
- description: "Committed subset has exactly one selector; the seller must not silently add the unselected collection"
-
- - id: read_selected_subset
- title: "Verify the persisted subset selection"
- task: get_media_buys
- schema_ref: "media-buy/get-media-buys-request.json"
- response_schema_ref: "media-buy/get-media-buys-response.json"
- doc_ref: "/media-buy/task-reference/get_media_buys"
- stateful: true
- sample_request:
- account:
- brand:
- domain: "acmeoutdoor.example"
- operator: "pinnacle-agency.example"
- operator_unit:
- id: "compliance-media_buy_seller_collection_selectio-7e6aac4c"
- sandbox: true
- media_buy_ids: ["$context.selected_media_buy_id"]
- expected: |
- Readback returns the same concrete, domain-qualified subset
- selector committed at create time.
+ mode: selected
+ collections: $context.__adcp_fixture_collection_subset
+ expected:
+ Accept the selection on the discovered product. Echo exactly the committed concrete
+ domain-qualified set in selected mode; default materializes the complete catalog
+ composition.
validations:
- check: response_schema
- description: "Response matches get-media-buys-response.json"
+ description: The response matches the canonical create-media-buy schema
+ - check: field_present
+ path: media_buy_id
+ description: The seller assigns the committed buy identity
- check: field_equals_context
- path: "media_buys[0].packages[0].package_id"
- context_key: "selected_package_id"
- description: "Readback targets the package created earlier"
- - check: field_value
- path: "media_buys[0].packages[0].targeting_overlay.collection_selection.mode"
- value: "selected"
- description: "Package readback MUST echo the committed collection selection (collections-and-installments.mdx, Collection targeting)"
+ path: packages[0].product_id
+ context_key: __adcp_fixture_collection_product_id
+ description: The response echoes the explicit SAME selected product identity
+ - check: array_length
+ path: packages
+ value: 1
+ description: The one-package request produces exactly one committed package
- check: field_value
- path: "media_buys[0].packages[0].targeting_overlay.collection_selection.collections[0].collection_ids[0]"
- value: "retro_news"
- description: "Readback echoes the committed collection as a concrete selector, not a re-resolvable reference (collections-and-installments.mdx, Collection targeting)"
- - check: field_absent
- path: "media_buys[0].packages[0].targeting_overlay.collection_selection.collections[0].collection_ids[1]"
- description: "Readback does not widen the committed one-collection subset"
-
- - id: default_bundle_materialization
- title: "mode: default materializes the full bundle as concrete selectors on readback"
- steps:
- - id: create_media_buy_default_bundle
- title: "Buy the product's default (full-bundle) collection selection"
- task: create_media_buy
- schema_ref: "media-buy/create-media-buy-request.json"
- response_schema_ref: "media-buy/create-media-buy-response.json"
- doc_ref: "/media-buy/task-reference/create_media_buy"
- stateful: true
- sample_request:
- brand:
- domain: "acmeoutdoor.example"
- account:
- brand:
- domain: "acmeoutdoor.example"
- operator: "pinnacle-agency.example"
- operator_unit:
- id: "compliance-media_buy_seller_collection_selectio-7e6aac4c"
- sandbox: true
- idempotency_key: "$generate:uuid_v4#collection_selection_default_bundle_create"
- start_time: "asap"
- end_time: "2099-09-30T23:59:59Z"
- packages:
- - product_id: "$context.bundle_product_id"
- pricing_option_id: "$context.bundle_pricing_option_id"
- bid_price: 22.0
- budget: 5000
- targeting_overlay:
- collection_selection:
- mode: "default"
- expected: |
- Accept the product's full bundle. Because the collection-selection
- schema's mode: "default" branch carries no collections field, the
- seller MUST echo the committed selection as concrete "selected"-shaped
- selectors naming both bundle collections rather than echoing
- mode: "default" back unmaterialized (core/collection-selection.json;
- collections-and-installments.mdx, Collection targeting).
+ path: packages[0].targeting_overlay.collection_selection.mode
+ value: selected
+ description: Package readback materializes concrete selected-form collection selectors
+ - check: collection_set_equals_context
+ path: packages[0].targeting_overlay.collection_selection.collections
+ context_key: __adcp_fixture_collection_subset
+ description:
+ The committed set exactly matches the selected product witness, preserving publisher
+ namespaces without widening
context_outputs:
- - key: "default_media_buy_id"
- path: "media_buy_id"
- - key: "default_package_id"
- path: "packages[0].package_id"
- validations:
- - check: response_schema
- description: "Response matches create-media-buy-response.json"
- - check: field_value
- path: "packages[0].targeting_overlay.collection_selection.mode"
- value: "selected"
- description: "Readback materializes mode: default into concrete selected-shaped selectors; the schema's default branch cannot carry a collections field (core/collection-selection.json), and sellers MUST echo the committed selection as concrete selectors (collections-and-installments.mdx, Collection targeting)"
- - check: field_contains
- path: "packages[0].targeting_overlay.collection_selection.collections[*].collection_ids[*]"
- value: "retro_news"
- description: "Materialized readback includes the full bundle's first collection"
- - check: field_contains
- path: "packages[0].targeting_overlay.collection_selection.collections[*].collection_ids[*]"
- value: "vintage_static"
- description: "Materialized readback includes the full bundle's second collection"
-
- - id: read_default_bundle
- title: "Verify the persisted materialized bundle selection"
- task: get_media_buys
- schema_ref: "media-buy/get-media-buys-request.json"
- response_schema_ref: "media-buy/get-media-buys-response.json"
- doc_ref: "/media-buy/task-reference/get_media_buys"
- stateful: true
- sample_request:
- account:
- brand:
- domain: "acmeoutdoor.example"
- operator: "pinnacle-agency.example"
- operator_unit:
- id: "compliance-media_buy_seller_collection_selectio-7e6aac4c"
- sandbox: true
- media_buy_ids: ["$context.default_media_buy_id"]
- expected: |
- Readback returns the same materialized full-bundle selection
- committed at create time.
- validations:
- - check: response_schema
- description: "Response matches get-media-buys-response.json"
- - check: field_equals_context
- path: "media_buys[0].packages[0].package_id"
- context_key: "default_package_id"
- description: "Readback targets the package created earlier"
- - check: field_value
- path: "media_buys[0].packages[0].targeting_overlay.collection_selection.mode"
- value: "selected"
- description: "Package readback MUST echo the committed collection selection as concrete selectors even when the buyer purchased mode: default (collections-and-installments.mdx, Collection targeting)"
- - check: field_contains
- path: "media_buys[0].packages[0].targeting_overlay.collection_selection.collections[*].collection_ids[*]"
- value: "retro_news"
- description: "Readback preserves the full bundle's first collection"
- - check: field_contains
- path: "media_buys[0].packages[0].targeting_overlay.collection_selection.collections[*].collection_ids[*]"
- value: "vintage_static"
- description: "Readback preserves the full bundle's second collection"
-
- - id: fixed_bundle_enforcement
- title: "A fixed (non-selectable) bundle rejects partial selection but accepts exact restatement"
- narrative: |
- When collection_targeting_allowed is false (the default), the product
- is a fixed bundle: a collection_selection that exactly restates the
- complete bundle remains an inherent match, but a narrower subset
- requires collection_targeting_allowed: true and must be rejected
- (core/product.json collection_targeting_allowed).
- steps:
- - id: reject_partial_selection_on_fixed_bundle
- title: "Reject a subset selection on the fixed bundle"
+ - name: collection_media_buy_id
+ path: media_buy_id
+ - name: collection_package_id
+ path: packages[0].package_id
+ - name: collection_committed_product_id
+ path: packages[0].product_id
+ - name: collection_committed_selectors
+ path: packages[0].targeting_overlay.collection_selection.collections
+ - id: reject_unknown_collection_id
+ title: Reject a collision-checked undeclared domain-qualified collection ID
task: create_media_buy
- schema_ref: "media-buy/create-media-buy-request.json"
- response_schema_ref: "media-buy/create-media-buy-response.json"
- doc_ref: "/media-buy/task-reference/create_media_buy"
+ schema_ref: media-buy/create-media-buy-request.json
+ response_schema_ref: media-buy/create-media-buy-response.json
+ doc_ref: /media-buy/task-reference/create_media_buy
+ stateful: true
expect_error: true
negative_path: payload_well_formed
- stateful: true
sample_request:
- brand:
- domain: "acmeoutdoor.example"
+ brand: $context.sandbox_brand
account:
brand:
- domain: "acmeoutdoor.example"
- operator: "pinnacle-agency.example"
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
operator_unit:
- id: "compliance-media_buy_seller_collection_selectio-7e6aac4c"
+ id: compliance-media_buy_seller_collection_selectio-7e6aac4c
sandbox: true
- idempotency_key: "$generate:uuid_v4#collection_selection_fixed_partial_reject"
- start_time: "asap"
+ idempotency_key: $generate:uuid_v4#collection_selection_reject_unknown_collection_id
+ start_time: asap
end_time: "2099-09-30T23:59:59Z"
packages:
- - product_id: "retro_bundle_ctv_fixed"
- pricing_option_id: "retro_bundle_fixed_cpm"
- bid_price: 22.0
- budget: 5000
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+ budget: $context.catalog_collection_budget
targeting_overlay:
collection_selection:
- mode: "selected"
- collections:
- - publisher_domain: "channel-owner.example"
- collection_ids: ["retro_news"]
- expected: |
- Reject with UNSUPPORTED_FEATURE. Partial collection selection
- requires collection_targeting_allowed: true; the fixed product
- cannot silently narrow to less than its complete bundle
- (core/product.json collection_targeting_allowed).
+ mode: selected
+ collections: $context.__adcp_fixture_collection_unknown_id
+ expected:
+ Reject with REFERENCE_NOT_FOUND; preserve the requested selectors rather than substituting
+ a supported set.
validations:
- check: error_code
- value: "UNSUPPORTED_FEATURE"
- description: "A fixed (collection_targeting_allowed: false) product rejects a narrower collection subset (core/product.json collection_targeting_allowed)"
-
- - id: accept_full_restatement_on_fixed_bundle
- title: "Accept an exact restatement of the fixed bundle's complete collections"
+ allowed_values:
+ - REFERENCE_NOT_FOUND
+ description:
+ The protocol distinguishes invalid shapes, unknown references and unsupported
+ fixed-bundle narrowing
+ - id: reject_unknown_publisher_domain
+ title: Reject a collection namespace absent from the SAME complete product
task: create_media_buy
- schema_ref: "media-buy/create-media-buy-request.json"
- response_schema_ref: "media-buy/create-media-buy-response.json"
- doc_ref: "/media-buy/task-reference/create_media_buy"
+ schema_ref: media-buy/create-media-buy-request.json
+ response_schema_ref: media-buy/create-media-buy-response.json
+ doc_ref: /media-buy/task-reference/create_media_buy
stateful: true
+ expect_error: true
+ negative_path: payload_well_formed
sample_request:
- brand:
- domain: "acmeoutdoor.example"
+ brand: $context.sandbox_brand
account:
brand:
- domain: "acmeoutdoor.example"
- operator: "pinnacle-agency.example"
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
operator_unit:
- id: "compliance-media_buy_seller_collection_selectio-7e6aac4c"
+ id: compliance-media_buy_seller_collection_selectio-7e6aac4c
sandbox: true
- idempotency_key: "$generate:uuid_v4#collection_selection_fixed_full_restatement_create"
- start_time: "asap"
+ idempotency_key: $generate:uuid_v4#collection_selection_reject_unknown_publisher_domain
+ start_time: asap
end_time: "2099-09-30T23:59:59Z"
packages:
- - product_id: "retro_bundle_ctv_fixed"
- pricing_option_id: "retro_bundle_fixed_cpm"
- bid_price: 22.0
- budget: 5000
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+ budget: $context.catalog_collection_budget
targeting_overlay:
collection_selection:
- mode: "selected"
- collections:
- - publisher_domain: "channel-owner.example"
- collection_ids: ["retro_news", "vintage_static"]
- expected: |
- Accept the exact restatement of the complete fixed bundle. A
- collection_selection that exactly restates the complete bundle
- remains an inherent match even without collection_targeting_allowed
- (core/product.json collection_targeting_allowed), and the response
- echoes the complete committed set.
+ mode: selected
+ collections: $context.__adcp_fixture_collection_unknown_domain
+ expected:
+ Reject with REFERENCE_NOT_FOUND; preserve the requested selectors rather than substituting
+ a supported set.
validations:
- - check: response_schema
- description: "Response matches create-media-buy-response.json"
- - check: field_contains
- path: "packages[0].targeting_overlay.collection_selection.collections[*].collection_ids[*]"
- value: "retro_news"
- description: "Exact restatement echoes the first bundle collection"
- - check: field_contains
- path: "packages[0].targeting_overlay.collection_selection.collections[*].collection_ids[*]"
- value: "vintage_static"
- description: "Exact restatement echoes the second bundle collection"
+ - check: error_code
+ allowed_values:
+ - REFERENCE_NOT_FOUND
+ description:
+ The protocol distinguishes invalid shapes, unknown references and unsupported
+ fixed-bundle narrowing
diff --git a/static/compliance/source/protocols/media-buy/scenarios/collection_selection_readback.yaml b/static/compliance/source/protocols/media-buy/scenarios/collection_selection_readback.yaml
new file mode 100644
index 00000000000..4c0faa62d08
--- /dev/null
+++ b/static/compliance/source/protocols/media-buy/scenarios/collection_selection_readback.yaml
@@ -0,0 +1,269 @@
+id: media_buy_seller/collection_selection_readback
+version: 1.0.0
+introduced_in: "3.3"
+title: Seller persists the exact catalog collection subset
+category: media_buy_seller
+summary: Independently catalog-qualifies persisted collection readback when get_media_buys is advertised.
+track: media_buy
+requires:
+ - controller
+required_tools:
+ - get_media_buys
+requires_capability:
+ path: media_buy.buying_modes
+ contains: wholesale
+narrative: >-
+ Applicability comes only from fully paginated, schema-valid wholesale sandbox discovery. Select
+ one seller-owned non-guaranteed product and a fixed-price CPM option from that SAME product. No
+ collection product or price is seeded. A completed valid no-match is fixture_unsatisfied: feature
+ not tested, not a pass or proof of nonsupport. Discovery, pagination, schema, capture and
+ selected-path failures remain failures.
+
+
+ This scope resolves independently of the other collection scopes. One-pair default and
+ fixed-restatement coverage do not require a two-pair subset fixture. Fixed targeting accepts
+ either literal false or omission of collection_targeting_allowed. Pair identity includes both
+ publisher_domain and collection_id; grouping, order and legal catalog redundancy do not change the
+ set.
+
+
+ This companion grades creation followed by persisted readback under its own advertised
+ get_media_buys root gate; the six core scopes remain independent of this optional reader. Required arrays, exact buy/package identities and concrete echoes
+ remain graded.
+ Seeded-only, brief-only, guaranteed and auction-priced collection support is outside this catalog
+ slice. A controller without seed_account yields fixture_seed_unsupported; an absent controller
+ yields missing_test_controller.
+
+
+ This readback companion requires only get_media_buys in its root tool gate. A missing reader is a
+ genuine native storyboard-level coverage gate, not a relabeled authored failure. An advertised
+ reader must preserve the committed set. Callers of individual steps must honor the same root gate;
+ the repository caller returns the unchanged native root-gate step before preparation when the
+ reader is absent. Standalone reads need trusted prior buy, package and committed-selector context;
+ they requalify the current native catalog and do not provide durable cross-request prepared-state
+ authority.
+agent:
+ interaction_model: media_buy_seller
+ capabilities:
+ - sells_media
+ examples:
+ - A seller offering native collection-bearing sandbox products
+caller:
+ role: buyer_agent
+ example: Pinnacle Agency (buyer)
+prerequisites:
+ description:
+ Establish one isolated sandbox account, then discover the native product and its scoped
+ price. The repository catalog fixture caller prepares this graph once for full or standalone
+ execution, derives a valid budget from its whole selected price snapshot, and projects canonical
+ full selectors from the protected declaration. Null caller-derived context defaults are
+ unprepared sentinels, never fabricated selectors. Direct SDK callers must perform the same
+ preparation and projection before consuming this storyboard.
+ test_kit: test-kits/acme-outdoor.yaml
+ controller_seeding: true
+fixtures:
+ accounts:
+ - account_id: sandbox_collection_selection_readback
+ fixture:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_collection_selectio-1d7f802e
+ sandbox: true
+ billing: operator
+ status: active
+ products:
+ - product_id: catalog_collection_product
+ pricing_options:
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+fixture_resolution:
+ products:
+ - handle: catalog_collection_product
+ strategies:
+ - discover
+ match:
+ - path: ""
+ operator: collection_selection_satisfies
+ value:
+ collection_targeting: selectable
+ minimum_pairs: 2
+ - path: /delivery_type
+ operator: equals
+ value: non_guaranteed
+ - path: /pricing_options
+ operator: any_match
+ where:
+ - path: /pricing_model
+ operator: equals
+ value: cpm
+ - path: /fixed_price
+ operator: present
+ context_outputs:
+ - key: __adcp_fixture_collection_declared
+ witness: declared_selectors
+ - key: __adcp_fixture_collection_product_id
+ path: product_id
+ - key: __adcp_fixture_collection_subset
+ witness: strict_subset
+ pricing_options:
+ - handle: catalog_collection_fixed_cpm
+ product_handle: catalog_collection_product
+ strategies:
+ - discover
+ match:
+ - path: /pricing_model
+ operator: equals
+ value: cpm
+ - path: /fixed_price
+ operator: present
+ context_outputs:
+ - key: __adcp_fixture_collection_price
+ snapshot: selected_entity
+context:
+ catalog_collection_budget: 1000
+ catalog_collection_full_selectors: null
+ brand:
+ domain: acmeoutdoor.example
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_collection_selectio-1d7f802e
+ sandbox: true
+ sandbox_account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_collection_selectio-1d7f802e
+ sandbox: true
+ sandbox_brand:
+ domain: acmeoutdoor.example
+phases:
+ - id: commit_selection
+ title: Commit and validate the selection before readback
+ steps:
+ - id: create_selection
+ title: Purchase the catalog-qualified concrete collection set
+ task: create_media_buy
+ schema_ref: media-buy/create-media-buy-request.json
+ response_schema_ref: media-buy/create-media-buy-response.json
+ doc_ref: /media-buy/task-reference/create_media_buy
+ stateful: true
+ sample_request:
+ brand: $context.sandbox_brand
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_collection_selectio-1d7f802e
+ sandbox: true
+ idempotency_key: $generate:uuid_v4#collection_selection_readback_create_selection
+ start_time: asap
+ end_time: "2099-09-30T23:59:59Z"
+ packages:
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+ budget: $context.catalog_collection_budget
+ targeting_overlay:
+ collection_selection:
+ mode: selected
+ collections: $context.__adcp_fixture_collection_subset
+ expected:
+ Accept the selection on the discovered product. Echo exactly the committed concrete
+ domain-qualified set in selected mode; default materializes the complete catalog
+ composition.
+ validations:
+ - check: response_schema
+ description: The response matches the canonical create-media-buy schema
+ - check: field_present
+ path: media_buy_id
+ description: The seller assigns the committed buy identity
+ - check: field_equals_context
+ path: packages[0].product_id
+ context_key: __adcp_fixture_collection_product_id
+ description: The response echoes the explicit SAME selected product identity
+ - check: array_length
+ path: packages
+ value: 1
+ description: The one-package request produces exactly one committed package
+ - check: field_value
+ path: packages[0].targeting_overlay.collection_selection.mode
+ value: selected
+ description: Package readback materializes concrete selected-form collection selectors
+ - check: collection_set_equals_context
+ path: packages[0].targeting_overlay.collection_selection.collections
+ context_key: __adcp_fixture_collection_subset
+ description:
+ The committed set exactly matches the selected product witness, preserving publisher
+ namespaces without widening
+ context_outputs:
+ - name: collection_media_buy_id
+ path: media_buy_id
+ - name: collection_package_id
+ path: packages[0].package_id
+ - name: collection_committed_product_id
+ path: packages[0].product_id
+ - name: collection_committed_selectors
+ path: packages[0].targeting_overlay.collection_selection.collections
+ - id: persisted_readback
+ title: Grade the persisted committed set
+ steps:
+ - id: read_selection
+ title: Read the persisted exact collection selection when the seller offers readback
+ task: get_media_buys
+ requires_tool: get_media_buys
+ schema_ref: media-buy/get-media-buys-request.json
+ response_schema_ref: media-buy/get-media-buys-response.json
+ doc_ref: /media-buy/task-reference/get_media_buys
+ stateful: true
+ sample_request:
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_collection_selectio-1d7f802e
+ sandbox: true
+ media_buy_ids:
+ - $context.collection_media_buy_id
+ expected:
+ Return exactly the requested buy and its committed package. Preserve the complete
+ domain-qualified collection set; an advertised broken readback remains a failure.
+ validations:
+ - check: response_schema
+ description: Readback matches the canonical get-media-buys schema
+ - check: array_length
+ path: media_buys
+ value: 1
+ description: Only the requested buy is returned
+ - check: field_equals_context
+ path: media_buys[0].media_buy_id
+ context_key: collection_media_buy_id
+ description: Readback belongs to the actual committed buy
+ - check: field_equals_context
+ path: media_buys[0].packages[0].product_id
+ context_key: collection_committed_product_id
+ description: Readback echoes the explicit committed product identity
+ - check: array_length
+ path: media_buys[0].packages
+ value: 1
+ description: The buy preserves exactly its one committed package
+ - check: field_equals_context
+ path: media_buys[0].packages[0].package_id
+ context_key: collection_package_id
+ description: Readback belongs to the actual committed package
+ - check: field_value
+ path: media_buys[0].packages[0].targeting_overlay.collection_selection.mode
+ value: selected
+ description: Readback contains concrete selected-form selectors, including materialized defaults
+ - check: collection_set_equals_context
+ path: media_buys[0].packages[0].targeting_overlay.collection_selection.collections
+ context_key: collection_committed_selectors
+ description:
+ Persisted selectors equal the committed set from the preceding successful create; a failed
+ create blocks full-run readback, and grouping or ordering may vary
diff --git a/static/compliance/source/protocols/media-buy/scenarios/fixed_bundle_enforcement.yaml b/static/compliance/source/protocols/media-buy/scenarios/fixed_bundle_enforcement.yaml
new file mode 100644
index 00000000000..a7d000bf952
--- /dev/null
+++ b/static/compliance/source/protocols/media-buy/scenarios/fixed_bundle_enforcement.yaml
@@ -0,0 +1,242 @@
+id: media_buy_seller/fixed_bundle_enforcement
+version: 1.0.0
+introduced_in: "3.3"
+title: Seller accepts a fixed catalog bundle and rejects a strict subset
+category: media_buy_seller
+summary: Independently grades fixed collection support with at least 2 distinct domain-qualified pairs.
+track: media_buy
+requires:
+ - controller
+required_tools:
+ - create_media_buy
+requires_capability:
+ path: media_buy.buying_modes
+ contains: wholesale
+narrative: >-
+ Applicability comes only from fully paginated, schema-valid wholesale sandbox discovery. Select
+ one seller-owned non-guaranteed product and a fixed-price CPM option from that SAME product. No
+ collection product or price is seeded. A completed valid no-match is fixture_unsatisfied: feature
+ not tested, not a pass or proof of nonsupport. Discovery, pagination, schema, capture and
+ selected-path failures remain failures.
+
+
+ This scope resolves independently of the other collection scopes. One-pair default and
+ fixed-restatement coverage do not require a two-pair subset fixture. Fixed targeting accepts
+ either literal false or omission of collection_targeting_allowed. Pair identity includes both
+ publisher_domain and collection_id; grouping, order and legal catalog redundancy do not change the
+ set.
+
+
+ This core scope grades create and rejection assertions independently of get_media_buys. Separate
+ readback companions cover persisted subset/default selection under their own advertised reader
+ gate. Required arrays, exact buy/package identities and concrete echoes remain graded.
+ Seeded-only, brief-only, guaranteed and auction-priced collection support is outside this catalog
+ slice. A controller without seed_account yields fixture_seed_unsupported; an absent controller
+ yields missing_test_controller.
+agent:
+ interaction_model: media_buy_seller
+ capabilities:
+ - sells_media
+ examples:
+ - A seller offering native collection-bearing sandbox products
+caller:
+ role: buyer_agent
+ example: Pinnacle Agency (buyer)
+prerequisites:
+ description:
+ Establish one isolated sandbox account, then discover the native product and its scoped
+ price. The repository catalog fixture caller prepares this graph once for full or standalone
+ execution, derives a valid budget from its whole selected price snapshot, and projects canonical
+ full selectors from the protected declaration. Null caller-derived context defaults are
+ unprepared sentinels, never fabricated selectors. Direct SDK callers must perform the same
+ preparation and projection before consuming this storyboard.
+ test_kit: test-kits/acme-outdoor.yaml
+ controller_seeding: true
+fixtures:
+ accounts:
+ - account_id: sandbox_fixed_bundle_enforcement
+ fixture:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_fixed_bundle_enforc-47911509
+ sandbox: true
+ billing: operator
+ status: active
+ products:
+ - product_id: catalog_collection_product
+ pricing_options:
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+fixture_resolution:
+ products:
+ - handle: catalog_collection_product
+ strategies:
+ - discover
+ match:
+ - path: ""
+ operator: collection_selection_satisfies
+ value:
+ collection_targeting: fixed
+ minimum_pairs: 2
+ - path: /delivery_type
+ operator: equals
+ value: non_guaranteed
+ - path: /pricing_options
+ operator: any_match
+ where:
+ - path: /pricing_model
+ operator: equals
+ value: cpm
+ - path: /fixed_price
+ operator: present
+ context_outputs:
+ - key: __adcp_fixture_collection_declared
+ witness: declared_selectors
+ - key: __adcp_fixture_collection_product_id
+ path: product_id
+ - key: __adcp_fixture_collection_subset
+ witness: strict_subset
+ pricing_options:
+ - handle: catalog_collection_fixed_cpm
+ product_handle: catalog_collection_product
+ strategies:
+ - discover
+ match:
+ - path: /pricing_model
+ operator: equals
+ value: cpm
+ - path: /fixed_price
+ operator: present
+ context_outputs:
+ - key: __adcp_fixture_collection_price
+ snapshot: selected_entity
+context:
+ catalog_collection_budget: 1000
+ catalog_collection_full_selectors: null
+ brand:
+ domain: acmeoutdoor.example
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_fixed_bundle_enforc-47911509
+ sandbox: true
+ sandbox_account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_fixed_bundle_enforc-47911509
+ sandbox: true
+ sandbox_brand:
+ domain: acmeoutdoor.example
+phases:
+ - id: commit_selection
+ title: Grade the independently selected collection product
+ steps:
+ - id: create_selection
+ title: Purchase the catalog-qualified concrete collection set
+ task: create_media_buy
+ schema_ref: media-buy/create-media-buy-request.json
+ response_schema_ref: media-buy/create-media-buy-response.json
+ doc_ref: /media-buy/task-reference/create_media_buy
+ stateful: true
+ sample_request:
+ brand: $context.sandbox_brand
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_fixed_bundle_enforc-47911509
+ sandbox: true
+ idempotency_key: $generate:uuid_v4#fixed_bundle_enforcement_create_selection
+ start_time: asap
+ end_time: "2099-09-30T23:59:59Z"
+ packages:
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+ budget: $context.catalog_collection_budget
+ targeting_overlay:
+ collection_selection:
+ mode: selected
+ collections: $context.catalog_collection_full_selectors
+ expected:
+ Accept the selection on the discovered product. Echo exactly the committed concrete
+ domain-qualified set in selected mode; default materializes the complete catalog
+ composition.
+ validations:
+ - check: response_schema
+ description: The response matches the canonical create-media-buy schema
+ - check: field_present
+ path: media_buy_id
+ description: The seller assigns the committed buy identity
+ - check: field_equals_context
+ path: packages[0].product_id
+ context_key: __adcp_fixture_collection_product_id
+ description: The response echoes the explicit SAME selected product identity
+ - check: array_length
+ path: packages
+ value: 1
+ description: The one-package request produces exactly one committed package
+ - check: field_value
+ path: packages[0].targeting_overlay.collection_selection.mode
+ value: selected
+ description: Package readback materializes concrete selected-form collection selectors
+ - check: collection_set_equals_context
+ path: packages[0].targeting_overlay.collection_selection.collections
+ context_key: __adcp_fixture_collection_declared
+ description:
+ The committed set exactly matches the selected product witness, preserving publisher
+ namespaces without widening
+ context_outputs:
+ - name: collection_media_buy_id
+ path: media_buy_id
+ - name: collection_package_id
+ path: packages[0].package_id
+ - name: collection_committed_product_id
+ path: packages[0].product_id
+ - name: collection_committed_selectors
+ path: packages[0].targeting_overlay.collection_selection.collections
+ - id: reject_fixed_subset
+ title: Reject a strict subset of this SAME fixed product
+ task: create_media_buy
+ schema_ref: media-buy/create-media-buy-request.json
+ response_schema_ref: media-buy/create-media-buy-response.json
+ doc_ref: /media-buy/task-reference/create_media_buy
+ stateful: true
+ expect_error: true
+ negative_path: payload_well_formed
+ sample_request:
+ brand: $context.sandbox_brand
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_fixed_bundle_enforc-47911509
+ sandbox: true
+ idempotency_key: $generate:uuid_v4#fixed_bundle_enforcement_reject_fixed_subset
+ start_time: asap
+ end_time: "2099-09-30T23:59:59Z"
+ packages:
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+ budget: $context.catalog_collection_budget
+ targeting_overlay:
+ collection_selection:
+ mode: selected
+ collections: $context.__adcp_fixture_collection_subset
+ expected:
+ Reject with UNSUPPORTED_FEATURE; preserve the requested selectors rather than substituting
+ a supported set.
+ validations:
+ - check: error_code
+ allowed_values:
+ - UNSUPPORTED_FEATURE
+ description:
+ The protocol distinguishes invalid shapes, unknown references and unsupported
+ fixed-bundle narrowing
diff --git a/static/compliance/source/protocols/media-buy/scenarios/fixed_bundle_restatement.yaml b/static/compliance/source/protocols/media-buy/scenarios/fixed_bundle_restatement.yaml
new file mode 100644
index 00000000000..171091a642f
--- /dev/null
+++ b/static/compliance/source/protocols/media-buy/scenarios/fixed_bundle_restatement.yaml
@@ -0,0 +1,201 @@
+id: media_buy_seller/fixed_bundle_restatement
+version: 1.0.0
+introduced_in: "3.3"
+title: Seller accepts an exact restatement of a fixed catalog collection bundle
+category: media_buy_seller
+summary: Independently grades fixed collection support with at least 1 distinct domain-qualified pair.
+track: media_buy
+requires:
+ - controller
+required_tools:
+ - create_media_buy
+requires_capability:
+ path: media_buy.buying_modes
+ contains: wholesale
+narrative: >-
+ Applicability comes only from fully paginated, schema-valid wholesale sandbox discovery. Select
+ one seller-owned non-guaranteed product and a fixed-price CPM option from that SAME product. No
+ collection product or price is seeded. A completed valid no-match is fixture_unsatisfied: feature
+ not tested, not a pass or proof of nonsupport. Discovery, pagination, schema, capture and
+ selected-path failures remain failures.
+
+
+ This scope resolves independently of the other collection scopes. One-pair default and
+ fixed-restatement coverage do not require a two-pair subset fixture. Fixed targeting accepts
+ either literal false or omission of collection_targeting_allowed. Pair identity includes both
+ publisher_domain and collection_id; grouping, order and legal catalog redundancy do not change the
+ set.
+
+
+ This core scope grades create and rejection assertions independently of get_media_buys. Separate
+ readback companions cover persisted subset/default selection under their own advertised reader
+ gate. Required arrays, exact buy/package identities and concrete echoes remain graded.
+ Seeded-only, brief-only, guaranteed and auction-priced collection support is outside this catalog
+ slice. A controller without seed_account yields fixture_seed_unsupported; an absent controller
+ yields missing_test_controller.
+agent:
+ interaction_model: media_buy_seller
+ capabilities:
+ - sells_media
+ examples:
+ - A seller offering native collection-bearing sandbox products
+caller:
+ role: buyer_agent
+ example: Pinnacle Agency (buyer)
+prerequisites:
+ description:
+ Establish one isolated sandbox account, then discover the native product and its scoped
+ price. The repository catalog fixture caller prepares this graph once for full or standalone
+ execution, derives a valid budget from its whole selected price snapshot, and projects canonical
+ full selectors from the protected declaration. Null caller-derived context defaults are
+ unprepared sentinels, never fabricated selectors. Direct SDK callers must perform the same
+ preparation and projection before consuming this storyboard.
+ test_kit: test-kits/acme-outdoor.yaml
+ controller_seeding: true
+fixtures:
+ accounts:
+ - account_id: sandbox_fixed_bundle_restatement
+ fixture:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_fixed_bundle_restat-2e84b25b
+ sandbox: true
+ billing: operator
+ status: active
+ products:
+ - product_id: catalog_collection_product
+ pricing_options:
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+fixture_resolution:
+ products:
+ - handle: catalog_collection_product
+ strategies:
+ - discover
+ match:
+ - path: ""
+ operator: collection_selection_satisfies
+ value:
+ collection_targeting: fixed
+ minimum_pairs: 1
+ - path: /delivery_type
+ operator: equals
+ value: non_guaranteed
+ - path: /pricing_options
+ operator: any_match
+ where:
+ - path: /pricing_model
+ operator: equals
+ value: cpm
+ - path: /fixed_price
+ operator: present
+ context_outputs:
+ - key: __adcp_fixture_collection_declared
+ witness: declared_selectors
+ - key: __adcp_fixture_collection_product_id
+ path: product_id
+ pricing_options:
+ - handle: catalog_collection_fixed_cpm
+ product_handle: catalog_collection_product
+ strategies:
+ - discover
+ match:
+ - path: /pricing_model
+ operator: equals
+ value: cpm
+ - path: /fixed_price
+ operator: present
+ context_outputs:
+ - key: __adcp_fixture_collection_price
+ snapshot: selected_entity
+context:
+ catalog_collection_budget: 1000
+ catalog_collection_full_selectors: null
+ brand:
+ domain: acmeoutdoor.example
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_fixed_bundle_restat-2e84b25b
+ sandbox: true
+ sandbox_account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_fixed_bundle_restat-2e84b25b
+ sandbox: true
+ sandbox_brand:
+ domain: acmeoutdoor.example
+phases:
+ - id: commit_selection
+ title: Grade the independently selected collection product
+ steps:
+ - id: create_selection
+ title: Purchase the catalog-qualified concrete collection set
+ task: create_media_buy
+ schema_ref: media-buy/create-media-buy-request.json
+ response_schema_ref: media-buy/create-media-buy-response.json
+ doc_ref: /media-buy/task-reference/create_media_buy
+ stateful: true
+ sample_request:
+ brand: $context.sandbox_brand
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_fixed_bundle_restat-2e84b25b
+ sandbox: true
+ idempotency_key: $generate:uuid_v4#fixed_bundle_restatement_create_selection
+ start_time: asap
+ end_time: "2099-09-30T23:59:59Z"
+ packages:
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+ budget: $context.catalog_collection_budget
+ targeting_overlay:
+ collection_selection:
+ mode: selected
+ collections: $context.catalog_collection_full_selectors
+ expected:
+ Accept the selection on the discovered product. Echo exactly the committed concrete
+ domain-qualified set in selected mode; default materializes the complete catalog
+ composition.
+ validations:
+ - check: response_schema
+ description: The response matches the canonical create-media-buy schema
+ - check: field_present
+ path: media_buy_id
+ description: The seller assigns the committed buy identity
+ - check: field_equals_context
+ path: packages[0].product_id
+ context_key: __adcp_fixture_collection_product_id
+ description: The response echoes the explicit SAME selected product identity
+ - check: array_length
+ path: packages
+ value: 1
+ description: The one-package request produces exactly one committed package
+ - check: field_value
+ path: packages[0].targeting_overlay.collection_selection.mode
+ value: selected
+ description: Package readback materializes concrete selected-form collection selectors
+ - check: collection_set_equals_context
+ path: packages[0].targeting_overlay.collection_selection.collections
+ context_key: __adcp_fixture_collection_declared
+ description:
+ The committed set exactly matches the selected product witness, preserving publisher
+ namespaces without widening
+ context_outputs:
+ - name: collection_media_buy_id
+ path: media_buy_id
+ - name: collection_package_id
+ path: packages[0].package_id
+ - name: collection_committed_product_id
+ path: packages[0].product_id
+ - name: collection_committed_selectors
+ path: packages[0].targeting_overlay.collection_selection.collections
diff --git a/static/compliance/source/protocols/media-buy/scenarios/owner_sold_channel_carriage.yaml b/static/compliance/source/protocols/media-buy/scenarios/owner_sold_channel_carriage.yaml
index f253629c35c..427bdfcf55a 100644
--- a/static/compliance/source/protocols/media-buy/scenarios/owner_sold_channel_carriage.yaml
+++ b/static/compliance/source/protocols/media-buy/scenarios/owner_sold_channel_carriage.yaml
@@ -1,388 +1,335 @@
id: media_buy_seller/owner_sold_channel_carriage
-version: "1.0.0"
+version: 1.0.1
introduced_in: "3.2"
-title: "Seller declares owner-sold channel carriage and enforces collection selector boundaries"
+title: Seller accepts declared cross-publisher collection selectors and rejects invalid selectors
category: media_buy_seller
-summary: "Verifies an owner-sold channel product combines host property scope with the owner's canonical collection selector, that buyer collection selection requires explicit collection_ids, and that unknown or mismatched collection selectors are rejected rather than silently substituted."
+summary:
+ Independently grades any collection support with at least 1 distinct domain-qualified pair
+ and a structural cross-publisher relation.
track: media_buy
requires:
- controller
-
required_tools:
- - get_products
- create_media_buy
- - comply_test_controller
+requires_capability:
+ path: media_buy.buying_modes
+ contains: wholesale
+narrative: >-
+ Applicability comes only from fully paginated, schema-valid wholesale sandbox discovery. Select
+ one seller-owned non-guaranteed product and a fixed-price CPM option from that SAME product. No
+ collection product or price is seeded. A completed valid no-match is fixture_unsatisfied: feature
+ not tested, not a pass or proof of nonsupport. Discovery, pagination, schema, capture and
+ selected-path failures remain failures.
+
+
+ This scope resolves independently of the other collection scopes. One-pair default and
+ fixed-restatement coverage do not require a two-pair subset fixture. Fixed targeting accepts
+ either literal false or omission of collection_targeting_allowed. Pair identity includes both
+ publisher_domain and collection_id; grouping, order and legal catalog redundancy do not change the
+ set.
-narrative: |
- A continuously programmed audio or video stream is modeled as a `kind:
- "channel"` collection declared by the channel owner, carried on a host CTV
- app property. When the owner sells its allocated avails, a verified
- owner-sold supply path requires the host's adagents.json to authorize the
- owner's sales agent for the host property, narrowed to the owner's
- canonical collection (docs/governance/property/adagents.mdx, "Owner-sold
- channel carriage"; docs/media-buy/product-discovery/collections-and-installments.mdx,
- "Programmed channels and owner-sold carriage").
- Buyer agents distinguish three verification states: verified owner-sold
- (host adagents.json authorizes the owner's agent, narrowed by an exact or
- bulk-grant collection selector), host-delegated (only an ads.txt/app-ads.txt
- `inventorypartnerdomain=` line corroborates the relationship), and
- owner-attested (only the owner's own `distribution[]` entry asserts
- carriage, with no host-side corroboration at all). Missing host
- authorization, a host selector naming a different owner domain, a dangling
- `distribution[].property_ids` value, and a typo'd collection_id are all
- fail-closed conditions on that ladder.
+ Cross-publisher means a retained collection namespace outside ALL publisher_properties domains of
+ the SAME product. The subset and undeclared-selector witnesses retain that relation; one external
+ plus one local pair suffices for a two-pair scope. This structural fact proves neither beneficial
+ ownership, host authorization, channel kind nor a verified supply path. This storyboard does not
+ fetch adagents.json or evaluate the owner-sold verification ladder.
- adagents.json itself is fetched directly by buyer SDKs over HTTPS and is
- not carried by any AdCP task — the verification ladder is buyer-side
- classification logic over two independently-hosted JSON documents. This
- storyboard exercises the portion of that model that *is* wire-visible: the
- seller's product declarations that a buyer's verification depends on, and
- the seller's obligation to reject collection selectors it cannot honor
- rather than silently substituting a different collection. It does not
- simulate adagents.json content itself; see the compliance report for what
- remains uncovered.
+ This core scope grades create and rejection assertions independently of get_media_buys. Separate
+ readback companions cover persisted subset/default selection under their own advertised reader
+ gate. Required arrays, exact buy/package identities and concrete echoes remain graded.
+ Seeded-only, brief-only, guaranteed and auction-priced collection support is outside this catalog
+ slice. A controller without seed_account yields fixture_seed_unsupported; an absent controller
+ yields missing_test_controller.
agent:
interaction_model: media_buy_seller
capabilities:
- sells_media
examples:
- - "A channel owner's sales agent selling owner-sold avails on a host CTV app"
-
+ - A seller offering native collection-bearing sandbox products
caller:
role: buyer_agent
- example: "Pinnacle Agency (buyer)"
-
+ example: Pinnacle Agency (buyer)
prerequisites:
- description: |
- The caller needs a brand identity for product discovery and buy creation.
- The controller seeds one owner-sold channel product that combines a host
- property selector with the owner's canonical collection selector, per the
- docs' worked example (hoststream.example / hoststream_ctv carrying
- channel-owner.example's retro_news channel).
- test_kit: "test-kits/acme-outdoor.yaml"
+ description:
+ Establish one isolated sandbox account, then discover the native product and its scoped
+ price. The repository catalog fixture caller prepares this graph once for full or standalone
+ execution, derives a valid budget from its whole selected price snapshot, and projects canonical
+ full selectors from the protected declaration. Null caller-derived context defaults are
+ unprepared sentinels, never fabricated selectors. Direct SDK callers must perform the same
+ preparation and projection before consuming this storyboard.
+ test_kit: test-kits/acme-outdoor.yaml
controller_seeding: true
-
fixtures:
accounts:
- - account_id: "acct_acmeoutdoor_example_sandbox_bd866a1fee90"
+ - account_id: sandbox_owner_sold_channel_carriage
fixture:
brand:
- domain: "acmeoutdoor.example"
- operator: "pinnacle-agency.example"
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
operator_unit:
- id: "compliance-media_buy_seller_owner_sold_channel_-f9c89a59"
- billing: "operator"
- status: "active"
+ id: compliance-media_buy_seller_owner_sold_channel_-f9c89a59
sandbox: true
+ billing: operator
+ status: active
products:
- - product_id: "retro_news_owner_sold"
- delivery_type: "non_guaranteed"
- channels: ["ctv"]
- format_options:
- - format_option_id: "video_30s"
- format_kind: "video_hosted"
- params:
- duration_ms_exact: 30000
- publisher_properties:
- - publisher_domain: "hoststream.example"
- selection_type: "by_id"
- property_ids: ["hoststream_ctv"]
- collections:
- - publisher_domain: "channel-owner.example"
- collection_ids: ["retro_news"]
- collection_targeting_allowed: true
- placements:
- - kind: "seller_inline"
- seller_agent:
- agent_url: "https://sales.channel-owner.example"
- publisher_domain: "channel-owner.example"
- placement_id: "owner_avails"
- name: "Channel-owner avails"
- mode: "included"
+ - product_id: catalog_collection_product
pricing_options:
- - product_id: "retro_news_owner_sold"
- pricing_option_id: "retro_news_cpm"
- pricing_model: "cpm"
- currency: "USD"
- floor_price: 20.0
-
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+fixture_resolution:
+ products:
+ - handle: catalog_collection_product
+ strategies:
+ - discover
+ match:
+ - path: ""
+ operator: collection_selection_satisfies
+ value:
+ collection_targeting: any
+ minimum_pairs: 1
+ require_cross_publisher_collection: true
+ - path: /delivery_type
+ operator: equals
+ value: non_guaranteed
+ - path: /pricing_options
+ operator: any_match
+ where:
+ - path: /pricing_model
+ operator: equals
+ value: cpm
+ - path: /fixed_price
+ operator: present
+ context_outputs:
+ - key: __adcp_fixture_collection_declared
+ witness: declared_selectors
+ - key: __adcp_fixture_collection_product_id
+ path: product_id
+ - key: __adcp_fixture_collection_relation
+ witness: cross_publisher_relation
+ - key: __adcp_fixture_collection_unknown_id
+ witness: undeclared_collection_id_selectors
+ - key: __adcp_fixture_collection_unknown_domain
+ witness: undeclared_publisher_domain_selectors
+ pricing_options:
+ - handle: catalog_collection_fixed_cpm
+ product_handle: catalog_collection_product
+ strategies:
+ - discover
+ match:
+ - path: /pricing_model
+ operator: equals
+ value: cpm
+ - path: /fixed_price
+ operator: present
+ context_outputs:
+ - key: __adcp_fixture_collection_price
+ snapshot: selected_entity
+context:
+ catalog_collection_budget: 1000
+ catalog_collection_full_selectors: null
+ catalog_collection_bulk_selectors: null
+ brand:
+ domain: acmeoutdoor.example
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_owner_sold_channel_-f9c89a59
+ sandbox: true
+ sandbox_account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_owner_sold_channel_-f9c89a59
+ sandbox: true
+ sandbox_brand:
+ domain: acmeoutdoor.example
phases:
- - id: verified_owner_sold_discovery
- title: "Discover the verified owner-sold channel product"
- narrative: |
- A verified owner-sold supply path requires the host property scope and
- the owner's external collection selector to appear together on the
- seller's product (collections-and-installments.mdx, "Programmed
- channels and owner-sold carriage"). This phase confirms the seller
- returns both halves of that combination on the same product.
+ - id: commit_selection
+ title: Grade the independently selected collection product
steps:
- - id: get_products_owner_sold_channel
- title: "Discover the owner-sold channel product"
- task: get_products
- schema_ref: "media-buy/get-products-request.json"
- response_schema_ref: "media-buy/get-products-response.json"
- doc_ref: "/media-buy/product-discovery/collections-and-installments"
- stateful: true
- sample_request:
- buying_mode: "brief"
- brief: "Pre-roll on Acme Retro News, the syndicated retro news channel carried on the HostStream CTV app"
- account:
- brand:
- domain: "acmeoutdoor.example"
- operator: "pinnacle-agency.example"
- operator_unit:
- id: "compliance-media_buy_seller_owner_sold_channel_-f9c89a59"
- sandbox: true
- filters:
- channels: ["ctv"]
- context:
- correlation_id: "owner_sold_channel_carriage--discovery"
- expected: |
- Return the owner-sold channel product. The product combines the
- host property scope (hoststream.example / hoststream_ctv) with the
- owner's canonical collection selector (channel-owner.example /
- retro_news) on the same product, and the collection selector names
- explicit collection_ids rather than the domain-only bulk-grant
- form.
- context_outputs:
- - key: "owner_sold_product_id"
- path: "products[0].product_id"
- - key: "owner_sold_pricing_option_id"
- path: "products[0].pricing_options[0].pricing_option_id"
- validations:
- - check: response_schema
- description: "Response matches get-products-response.json"
- - check: field_value
- path: "products[0].publisher_properties[0].publisher_domain"
- value: "hoststream.example"
- description: "Product declares the host property scope a verified owner-sold supply path requires (collections-and-installments.mdx, Programmed channels and owner-sold carriage)"
- - check: field_value
- path: "products[0].collections[0].publisher_domain"
- value: "channel-owner.example"
- description: "Collection selector names the owner's adagents.json domain (core/collection-selector.json requires publisher_domain)"
- - check: field_present
- path: "products[0].collections[0].collection_ids"
- description: "Product collection selectors MUST name explicit collection_ids; the domain-only bulk-grant form is authorization scoping only, not product composition (core/product.json collections[] requires collection_ids)"
- - check: field_value
- path: "products[0].collections[0].collection_ids[0]"
- value: "retro_news"
- description: "Product references the owner's canonical collection matching the verified-path worked example"
- - check: field_value
- path: "products[0].collection_targeting_allowed"
- value: true
- description: "Product advertises collection_targeting_allowed so buyers can exercise targeting_overlay.collection_selection (core/product.json collection_targeting_allowed)"
-
- - id: verified_selection_accepted
- title: "Accept the correct owner collection selection and echo it back"
- narrative: |
- Before probing the fail-closed legs, prove the affirmative path: the
- exact selector the product declares is accepted, and the package
- readback echoes the committed selection as concrete selectors
- (core/package.json targeting_overlay; core/collection-selection.json).
- Without this leg, a seller rejecting every create would pass the
- rejection phases vacuously.
- steps:
- - id: create_media_buy_correct_selector
- title: "Buy the owner-sold channel with the declared collection selector"
+ - id: create_selection
+ title: Purchase the catalog-qualified concrete collection set
task: create_media_buy
- schema_ref: "media-buy/create-media-buy-request.json"
- response_schema_ref: "media-buy/create-media-buy-response.json"
- doc_ref: "/media-buy/task-reference/create_media_buy"
+ schema_ref: media-buy/create-media-buy-request.json
+ response_schema_ref: media-buy/create-media-buy-response.json
+ doc_ref: /media-buy/task-reference/create_media_buy
stateful: true
sample_request:
- brand:
- domain: "acmeoutdoor.example"
+ brand: $context.sandbox_brand
account:
brand:
- domain: "acmeoutdoor.example"
- operator: "pinnacle-agency.example"
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
operator_unit:
- id: "compliance-media_buy_seller_owner_sold_channel_-f9c89a59"
+ id: compliance-media_buy_seller_owner_sold_channel_-f9c89a59
sandbox: true
- idempotency_key: "$generate:uuid_v4#owner_sold_channel_carriage_accept"
- start_time: "asap"
+ idempotency_key: $generate:uuid_v4#owner_sold_channel_carriage_create_selection
+ start_time: asap
end_time: "2099-09-30T23:59:59Z"
packages:
- - product_id: "$context.owner_sold_product_id"
- pricing_option_id: "$context.owner_sold_pricing_option_id"
- bid_price: 20.0
- budget: 5000
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+ budget: $context.catalog_collection_budget
targeting_overlay:
collection_selection:
- mode: "selected"
- collections:
- - publisher_domain: "channel-owner.example"
- collection_ids: ["retro_news"]
- expected: |
- Accept the buy. The committed collection selection is echoed in the
- package's targeting_overlay.collection_selection as the same
- concrete domain-qualified selector, unchanged and unwidened.
+ mode: selected
+ collections: $context.catalog_collection_full_selectors
+ expected:
+ Accept the selection on the discovered product. Echo exactly the committed concrete
+ domain-qualified set in selected mode; default materializes the complete catalog
+ composition.
validations:
- check: response_schema
- description: "Response matches create-media-buy-response.json"
- - check: field_value
- path: "packages[0].targeting_overlay.collection_selection.mode"
- value: "selected"
- description: "Package readback MUST echo the committed collection selection as concrete selectors (core/package.json targeting_overlay)"
- - check: field_value
- path: "packages[0].targeting_overlay.collection_selection.collections[0].publisher_domain"
- value: "channel-owner.example"
- description: "Echo preserves the owner's publisher namespace (core/collection-selector.json)"
+ description: The response matches the canonical create-media-buy schema
+ - check: field_present
+ path: media_buy_id
+ description: The seller assigns the committed buy identity
+ - check: field_equals_context
+ path: packages[0].product_id
+ context_key: __adcp_fixture_collection_product_id
+ description: The response echoes the explicit SAME selected product identity
+ - check: array_length
+ path: packages
+ value: 1
+ description: The one-package request produces exactly one committed package
- check: field_value
- path: "packages[0].targeting_overlay.collection_selection.collections[0].collection_ids[0]"
- value: "retro_news"
- description: "Echo names the committed collection exactly, not a substitute (core/collection-selection.json)"
-
- - id: explicit_selection_required
- title: "Buyer collection selection requires explicit collection_ids and known selectors"
- narrative: |
- mode: selected names the complete required set as domain-qualified
- selectors with explicit collection_ids; the domain-only bulk-grant form
- is authorization scoping, not selection (collections-and-installments.mdx,
- "Collection targeting"). Sellers reject unknown, duplicate, or
- partially applicable selectors rather than silently changing the
- selection (core/collection-selection.json). This phase exercises both
- rules: the bulk-grant selector shape used where authorization scoping
- belongs, not buyer selection; and two fail-closed legs where a
- schema-valid but unrecognized selector (a typo'd collection_id, or a
- selector naming a different owner domain than the product declares)
- must be rejected rather than silently mapped to the product's real
- collection.
- steps:
- - id: reject_bulk_grant_form_selection
- title: "Reject a domain-only (bulk-grant) collection selector in mode: selected"
+ path: packages[0].targeting_overlay.collection_selection.mode
+ value: selected
+ description: Package readback materializes concrete selected-form collection selectors
+ - check: collection_set_equals_context
+ path: packages[0].targeting_overlay.collection_selection.collections
+ context_key: __adcp_fixture_collection_declared
+ description:
+ The committed set exactly matches the selected product witness, preserving publisher
+ namespaces without widening
+ context_outputs:
+ - name: collection_media_buy_id
+ path: media_buy_id
+ - name: collection_package_id
+ path: packages[0].package_id
+ - name: collection_committed_product_id
+ path: packages[0].product_id
+ - name: collection_committed_selectors
+ path: packages[0].targeting_overlay.collection_selection.collections
+ - id: reject_bulk_grant_form
+ title: Reject an authorization-only domain selector as buyer selection
task: create_media_buy
- schema_ref: "media-buy/create-media-buy-request.json"
- response_schema_ref: "media-buy/create-media-buy-response.json"
- doc_ref: "/media-buy/task-reference/create_media_buy"
+ schema_ref: media-buy/create-media-buy-request.json
+ response_schema_ref: media-buy/create-media-buy-response.json
+ doc_ref: /media-buy/task-reference/create_media_buy
+ stateful: true
expect_error: true
negative_path: schema_invalid
- stateful: true
sample_request:
- brand:
- domain: "acmeoutdoor.example"
+ brand: $context.sandbox_brand
account:
brand:
- domain: "acmeoutdoor.example"
- operator: "pinnacle-agency.example"
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
operator_unit:
- id: "compliance-media_buy_seller_owner_sold_channel_-f9c89a59"
+ id: compliance-media_buy_seller_owner_sold_channel_-f9c89a59
sandbox: true
- idempotency_key: "$generate:uuid_v4#owner_sold_channel_carriage_bulk_form_reject"
- start_time: "asap"
+ idempotency_key: $generate:uuid_v4#owner_sold_channel_carriage_reject_bulk_grant_form
+ start_time: asap
end_time: "2099-09-30T23:59:59Z"
packages:
- - product_id: "$context.owner_sold_product_id"
- pricing_option_id: "$context.owner_sold_pricing_option_id"
- bid_price: 20.0
- budget: 5000
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+ budget: $context.catalog_collection_budget
targeting_overlay:
collection_selection:
- mode: "selected"
- collections:
- - publisher_domain: "channel-owner.example"
- expected: |
- Reject with INVALID_REQUEST. The domain-only bulk-grant selector
- form is valid only for adagents.json authorization scoping; every
- "selected" collection item MUST carry explicit collection_ids
- (core/collection-selection.json: selected-mode items require
- collection_ids). Buyer selection is never expressed with the
- bulk-grant form (collections-and-installments.mdx, "Collection
- targeting").
+ mode: selected
+ collections: $context.catalog_collection_bulk_selectors
+ expected:
+ Reject with INVALID_REQUEST; preserve the requested selectors rather than substituting a
+ supported set.
validations:
- check: error_code
- allowed_values: ["INVALID_REQUEST"]
- description: "A selected-mode collection selector omitting collection_ids is schema-invalid (core/collection-selection.json requires collection_ids on every selected item)"
-
+ allowed_values:
+ - INVALID_REQUEST
+ description:
+ The protocol distinguishes invalid shapes, unknown references and unsupported
+ fixed-bundle narrowing
- id: reject_unknown_collection_id
- title: "Reject a collection_id the product does not declare"
+ title: Reject a collision-checked undeclared domain-qualified collection ID
task: create_media_buy
- schema_ref: "media-buy/create-media-buy-request.json"
- response_schema_ref: "media-buy/create-media-buy-response.json"
- doc_ref: "/media-buy/task-reference/create_media_buy"
+ schema_ref: media-buy/create-media-buy-request.json
+ response_schema_ref: media-buy/create-media-buy-response.json
+ doc_ref: /media-buy/task-reference/create_media_buy
+ stateful: true
expect_error: true
negative_path: payload_well_formed
- stateful: true
sample_request:
- brand:
- domain: "acmeoutdoor.example"
+ brand: $context.sandbox_brand
account:
brand:
- domain: "acmeoutdoor.example"
- operator: "pinnacle-agency.example"
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
operator_unit:
- id: "compliance-media_buy_seller_owner_sold_channel_-f9c89a59"
+ id: compliance-media_buy_seller_owner_sold_channel_-f9c89a59
sandbox: true
- idempotency_key: "$generate:uuid_v4#owner_sold_channel_carriage_unknown_id_reject"
- start_time: "asap"
+ idempotency_key: $generate:uuid_v4#owner_sold_channel_carriage_reject_unknown_collection_id
+ start_time: asap
end_time: "2099-09-30T23:59:59Z"
packages:
- - product_id: "$context.owner_sold_product_id"
- pricing_option_id: "$context.owner_sold_pricing_option_id"
- bid_price: 20.0
- budget: 5000
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+ budget: $context.catalog_collection_budget
targeting_overlay:
collection_selection:
- mode: "selected"
- collections:
- - publisher_domain: "channel-owner.example"
- collection_ids: ["$generate:uuid_v4#owner_sold_unknown_collection"]
- expected: |
- Reject with INVALID_REQUEST. A collection_id the product's owner
- never declared — e.g. a typo of the real collection_id — must fail
- closed rather than being silently mapped to the product's actual
- collection (core/collection-selection.json: sellers reject unknown
- selectors rather than silently changing the selection).
+ mode: selected
+ collections: $context.__adcp_fixture_collection_unknown_id
+ expected:
+ Reject with REFERENCE_NOT_FOUND; preserve the requested selectors rather than substituting
+ a supported set.
validations:
- check: error_code
- allowed_values: ["REFERENCE_NOT_FOUND", "INVALID_REQUEST"]
- description: "An unrecognized collection_id is an unknown selector the seller must reject, not silently substitute (core/collection-selection.json); REFERENCE_NOT_FOUND is the registry-preferred code for a schema-valid dangling reference (enums/error-code.json)"
-
- - id: reject_mismatched_owner_domain
- title: "Reject a collection selector naming a different owner domain"
+ allowed_values:
+ - REFERENCE_NOT_FOUND
+ description:
+ The protocol distinguishes invalid shapes, unknown references and unsupported
+ fixed-bundle narrowing
+ - id: reject_unknown_publisher_domain
+ title: Reject a collection namespace absent from the SAME complete product
task: create_media_buy
- schema_ref: "media-buy/create-media-buy-request.json"
- response_schema_ref: "media-buy/create-media-buy-response.json"
- doc_ref: "/media-buy/task-reference/create_media_buy"
+ schema_ref: media-buy/create-media-buy-request.json
+ response_schema_ref: media-buy/create-media-buy-response.json
+ doc_ref: /media-buy/task-reference/create_media_buy
+ stateful: true
expect_error: true
negative_path: payload_well_formed
- stateful: true
sample_request:
- brand:
- domain: "acmeoutdoor.example"
+ brand: $context.sandbox_brand
account:
brand:
- domain: "acmeoutdoor.example"
- operator: "pinnacle-agency.example"
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
operator_unit:
- id: "compliance-media_buy_seller_owner_sold_channel_-f9c89a59"
+ id: compliance-media_buy_seller_owner_sold_channel_-f9c89a59
sandbox: true
- idempotency_key: "$generate:uuid_v4#owner_sold_channel_carriage_mismatched_domain_reject"
- start_time: "asap"
+ idempotency_key: $generate:uuid_v4#owner_sold_channel_carriage_reject_unknown_publisher_domain
+ start_time: asap
end_time: "2099-09-30T23:59:59Z"
packages:
- - product_id: "$context.owner_sold_product_id"
- pricing_option_id: "$context.owner_sold_pricing_option_id"
- bid_price: 20.0
- budget: 5000
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+ budget: $context.catalog_collection_budget
targeting_overlay:
collection_selection:
- mode: "selected"
- collections:
- - publisher_domain: "unrelated-owner.example"
- collection_ids: ["retro_news"]
- expected: |
- Reject with INVALID_REQUEST. A verified owner-sold supply path
- requires the host property scope and the owner's external
- collection selector to match the seller's product
- (collections-and-installments.mdx, "Programmed channels and
- owner-sold carriage"); a selector naming a publisher_domain the
- product does not declare is an unknown selector the seller must
- reject rather than resolve against its real owner
- (core/collection-selection.json).
+ mode: selected
+ collections: $context.__adcp_fixture_collection_unknown_domain
+ expected:
+ Reject with REFERENCE_NOT_FOUND; preserve the requested selectors rather than substituting
+ a supported set.
validations:
- check: error_code
- allowed_values: ["REFERENCE_NOT_FOUND", "INVALID_REQUEST"]
- description: "A collection selector naming an owner domain the product does not declare is an unknown selector (core/collection-selection.json) and breaks the host-property/owner-collection match a verified supply path requires (collections-and-installments.mdx); REFERENCE_NOT_FOUND is the registry-preferred code for a schema-valid dangling reference (enums/error-code.json)"
+ allowed_values:
+ - REFERENCE_NOT_FOUND
+ description:
+ The protocol distinguishes invalid shapes, unknown references and unsupported
+ fixed-bundle narrowing
diff --git a/static/compliance/source/protocols/media-buy/scenarios/owner_sold_channel_subset.yaml b/static/compliance/source/protocols/media-buy/scenarios/owner_sold_channel_subset.yaml
new file mode 100644
index 00000000000..2c92736f448
--- /dev/null
+++ b/static/compliance/source/protocols/media-buy/scenarios/owner_sold_channel_subset.yaml
@@ -0,0 +1,215 @@
+id: media_buy_seller/owner_sold_channel_subset
+version: 1.0.0
+introduced_in: "3.3"
+title: Seller preserves a cross-publisher collection when accepting a catalog subset
+category: media_buy_seller
+summary:
+ Independently grades selectable collection support with at least 2 distinct
+ domain-qualified pairs and a structural cross-publisher relation.
+track: media_buy
+requires:
+ - controller
+required_tools:
+ - create_media_buy
+requires_capability:
+ path: media_buy.buying_modes
+ contains: wholesale
+narrative: >-
+ Applicability comes only from fully paginated, schema-valid wholesale sandbox discovery. Select
+ one seller-owned non-guaranteed product and a fixed-price CPM option from that SAME product. No
+ collection product or price is seeded. A completed valid no-match is fixture_unsatisfied: feature
+ not tested, not a pass or proof of nonsupport. Discovery, pagination, schema, capture and
+ selected-path failures remain failures.
+
+
+ This scope resolves independently of the other collection scopes. One-pair default and
+ fixed-restatement coverage do not require a two-pair subset fixture. Fixed targeting accepts
+ either literal false or omission of collection_targeting_allowed. Pair identity includes both
+ publisher_domain and collection_id; grouping, order and legal catalog redundancy do not change the
+ set.
+
+
+ Cross-publisher means a retained collection namespace outside ALL publisher_properties domains of
+ the SAME product. The subset and undeclared-selector witnesses retain that relation; one external
+ plus one local pair suffices for a two-pair scope. This structural fact proves neither beneficial
+ ownership, host authorization, channel kind nor a verified supply path. This storyboard does not
+ fetch adagents.json or evaluate the owner-sold verification ladder.
+
+
+ This core scope grades create and rejection assertions independently of get_media_buys. Separate
+ readback companions cover persisted subset/default selection under their own advertised reader
+ gate. Required arrays, exact buy/package identities and concrete echoes remain graded.
+ Seeded-only, brief-only, guaranteed and auction-priced collection support is outside this catalog
+ slice. A controller without seed_account yields fixture_seed_unsupported; an absent controller
+ yields missing_test_controller.
+agent:
+ interaction_model: media_buy_seller
+ capabilities:
+ - sells_media
+ examples:
+ - A seller offering native collection-bearing sandbox products
+caller:
+ role: buyer_agent
+ example: Pinnacle Agency (buyer)
+prerequisites:
+ description:
+ Establish one isolated sandbox account, then discover the native product and its scoped
+ price. The repository catalog fixture caller prepares this graph once for full or standalone
+ execution, derives a valid budget from its whole selected price snapshot, and projects canonical
+ full selectors from the protected declaration. Null caller-derived context defaults are
+ unprepared sentinels, never fabricated selectors. Direct SDK callers must perform the same
+ preparation and projection before consuming this storyboard.
+ test_kit: test-kits/acme-outdoor.yaml
+ controller_seeding: true
+fixtures:
+ accounts:
+ - account_id: sandbox_owner_sold_channel_subset
+ fixture:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_owner_sold_channel_-7c50bf9a
+ sandbox: true
+ billing: operator
+ status: active
+ products:
+ - product_id: catalog_collection_product
+ pricing_options:
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+fixture_resolution:
+ products:
+ - handle: catalog_collection_product
+ strategies:
+ - discover
+ match:
+ - path: ""
+ operator: collection_selection_satisfies
+ value:
+ collection_targeting: selectable
+ minimum_pairs: 2
+ require_cross_publisher_collection: true
+ - path: /delivery_type
+ operator: equals
+ value: non_guaranteed
+ - path: /pricing_options
+ operator: any_match
+ where:
+ - path: /pricing_model
+ operator: equals
+ value: cpm
+ - path: /fixed_price
+ operator: present
+ context_outputs:
+ - key: __adcp_fixture_collection_declared
+ witness: declared_selectors
+ - key: __adcp_fixture_collection_product_id
+ path: product_id
+ - key: __adcp_fixture_collection_subset
+ witness: strict_subset
+ - key: __adcp_fixture_collection_relation
+ witness: cross_publisher_relation
+ pricing_options:
+ - handle: catalog_collection_fixed_cpm
+ product_handle: catalog_collection_product
+ strategies:
+ - discover
+ match:
+ - path: /pricing_model
+ operator: equals
+ value: cpm
+ - path: /fixed_price
+ operator: present
+ context_outputs:
+ - key: __adcp_fixture_collection_price
+ snapshot: selected_entity
+context:
+ catalog_collection_budget: 1000
+ catalog_collection_full_selectors: null
+ brand:
+ domain: acmeoutdoor.example
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_owner_sold_channel_-7c50bf9a
+ sandbox: true
+ sandbox_account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_owner_sold_channel_-7c50bf9a
+ sandbox: true
+ sandbox_brand:
+ domain: acmeoutdoor.example
+phases:
+ - id: commit_selection
+ title: Grade the independently selected collection product
+ steps:
+ - id: create_selection
+ title: Purchase the catalog-qualified concrete collection set
+ task: create_media_buy
+ schema_ref: media-buy/create-media-buy-request.json
+ response_schema_ref: media-buy/create-media-buy-response.json
+ doc_ref: /media-buy/task-reference/create_media_buy
+ stateful: true
+ sample_request:
+ brand: $context.sandbox_brand
+ account:
+ brand:
+ domain: acmeoutdoor.example
+ operator: pinnacle-agency.example
+ operator_unit:
+ id: compliance-media_buy_seller_owner_sold_channel_-7c50bf9a
+ sandbox: true
+ idempotency_key: $generate:uuid_v4#owner_sold_channel_subset_create_selection
+ start_time: asap
+ end_time: "2099-09-30T23:59:59Z"
+ packages:
+ - product_id: catalog_collection_product
+ pricing_option_id: catalog_collection_fixed_cpm
+ budget: $context.catalog_collection_budget
+ targeting_overlay:
+ collection_selection:
+ mode: selected
+ collections: $context.__adcp_fixture_collection_subset
+ expected:
+ Accept the selection on the discovered product. Echo exactly the committed concrete
+ domain-qualified set in selected mode; default materializes the complete catalog
+ composition.
+ validations:
+ - check: response_schema
+ description: The response matches the canonical create-media-buy schema
+ - check: field_present
+ path: media_buy_id
+ description: The seller assigns the committed buy identity
+ - check: field_equals_context
+ path: packages[0].product_id
+ context_key: __adcp_fixture_collection_product_id
+ description: The response echoes the explicit SAME selected product identity
+ - check: array_length
+ path: packages
+ value: 1
+ description: The one-package request produces exactly one committed package
+ - check: field_value
+ path: packages[0].targeting_overlay.collection_selection.mode
+ value: selected
+ description: Package readback materializes concrete selected-form collection selectors
+ - check: collection_set_equals_context
+ path: packages[0].targeting_overlay.collection_selection.collections
+ context_key: __adcp_fixture_collection_subset
+ description:
+ The committed set exactly matches the selected product witness, preserving publisher
+ namespaces without widening
+ context_outputs:
+ - name: collection_media_buy_id
+ path: media_buy_id
+ - name: collection_package_id
+ path: packages[0].package_id
+ - name: collection_committed_product_id
+ path: packages[0].product_id
+ - name: collection_committed_selectors
+ path: packages[0].targeting_overlay.collection_selection.collections
diff --git a/tests/addie/member-tools.test.ts b/tests/addie/member-tools.test.ts
index f203e86b5e3..e9d8cd28025 100644
--- a/tests/addie/member-tools.test.ts
+++ b/tests/addie/member-tools.test.ts
@@ -8,6 +8,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { MemberContext } from '../../server/src/addie/member-context.js';
import { githubIssueReceiptFromHandlerResult } from '../../server/src/addie/github-issue-receipt.js';
+import { execute as executeCollectionFixture } from '../optional-product-collections.test.cjs';
const memberToolMocks = vi.hoisted(() => ({
checkToolRateLimit: vi.fn(),
@@ -19,6 +20,8 @@ const memberToolMocks = vi.hoisted(() => ({
getComplianceStoryboardById: vi.fn(),
runStoryboard: vi.fn(),
runStoryboardStep: vi.fn(),
+ realRunStoryboardStep: null as unknown as typeof import("@adcp/sdk/testing").runStoryboardStep,
+ transportOptions: {} as Record,
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
}));
@@ -60,8 +63,14 @@ vi.mock('../../server/src/services/badge-issuance.js', async (importOriginal) =>
};
});
+vi.mock('../../server/src/utils/sdk-safe-fetch.js', async (importOriginal) => ({
+ ...await importOriginal() as Record,
+ withSdkSafeTransport: (options: Record) => ({ ...options, ...memberToolMocks.transportOptions }),
+}));
+
vi.mock('@adcp/sdk/testing', async (importOriginal) => {
const actual = await importOriginal() as Record;
+ memberToolMocks.realRunStoryboardStep = actual.runStoryboardStep as typeof memberToolMocks.realRunStoryboardStep;
return {
...actual,
getComplianceStoryboardById: memberToolMocks.getComplianceStoryboardById,
@@ -83,6 +92,7 @@ import * as complianceTesting from '../../server/src/addie/services/compliance-t
import * as wgService from '../../server/src/services/working-group-membership-service.js';
beforeEach(() => {
+ memberToolMocks.transportOptions = {};
memberToolMocks.getComplianceStoryboardById.mockReset();
memberToolMocks.runStoryboard.mockReset();
memberToolMocks.runStoryboardStep.mockReset();
@@ -237,6 +247,47 @@ describe('createMemberToolHandlers', () => {
}
});
+ describe('collection continuation integrity through the actual opaque handoff', () => {
+ for (const name of ['collection_selection_readback', 'collection_default_materialization_readback']) {
+ for (const failedCreate of [false, true]) {
+ it(`${name}: ${failedCreate ? 'failed' : 'successful'} public create round-trips through the handler`, async () => {
+ // Only the controlled seller transport and authored file lookup are
+ // injected. Handler sealing/resolution, adapter preparation/history
+ // checks and public SDK execution all run normally.
+ const scenario = await executeCollectionFixture(name, { scenarioOnly: true, ...(failedCreate && { broken: 'domain' }) });
+ memberToolMocks.getComplianceStoryboardById.mockReturnValue(scenario.doc);
+ memberToolMocks.transportOptions = {
+ ...scenario.options,
+ // The current source schema is the artifact under test; hosted
+ // target selection and envelope caller/run binding remain actual.
+ schemaRoot: undefined, servedReleaseSchemaRoots: undefined,
+ };
+ memberToolMocks.runStoryboardStep.mockImplementation(memberToolMocks.realRunStoryboardStep);
+ const handler = createMemberToolHandlers(null).get('run_storyboard_step')!;
+ const input = { agent_url: scenario.url, storyboard_id: scenario.doc.id, compliance_target: '3.0' };
+ const created = await handler({ ...input, step_id: 'create_selection' });
+ expect(created).toContain(failedCreate ? '[FAIL]' : '[PASS]');
+ if (failedCreate) expect(created).toContain('wrong-namespace.example');
+ const match = created.match(/\n([\s\S]*?)\n<\/context>/);
+ expect(match).not.toBeNull();
+ const context = JSON.parse(match![1]);
+ expect(Object.keys(context)).toEqual(['context_ref']);
+ expect(context.context_ref).toMatch(/^sctx1\./);
+ const before = scenario.calls.length;
+ if (failedCreate) {
+ await expect(handler({ ...input, step_id: 'read_selection', context }))
+ .rejects.toThrow(/requires trusted prior/);
+ expect(scenario.calls.slice(before).some((call: { task: string }) => call.task === 'get_media_buys')).toBe(false);
+ } else {
+ const read = await handler({ ...input, step_id: 'read_selection', context });
+ expect(read).toContain('[PASS]');
+ expect(scenario.calls.slice(before).some((call: { task: string }) => call.task === 'get_media_buys')).toBe(true);
+ }
+ }, 30000);
+ }
+ }
+ });
+
describe('storyboard diagnostic formatting', () => {
const storyboard = {
id: 'sb_demo',
diff --git a/tests/lint-storyboard-fixture-resolution.test.cjs b/tests/lint-storyboard-fixture-resolution.test.cjs
index 5d6352ece27..753d83112e2 100644
--- a/tests/lint-storyboard-fixture-resolution.test.cjs
+++ b/tests/lint-storyboard-fixture-resolution.test.cjs
@@ -138,3 +138,22 @@ fixture_resolution:
strategies: [seed]
`), []);
});
+
+function collectionDoc(value) {
+ return { fixtures: { products: [{ product_id: 'p' }] }, fixture_resolution: {
+ products: [{ handle: 'p', strategies: ['discover'], match: [{ path: '', operator: 'collection_selection_satisfies', value }] }],
+ } };
+}
+test('merged collection predicate accepts root pointer and authored optional defaults', () => {
+ for (const collection_targeting of ['fixed', 'selectable', 'any']) {
+ assert.deepEqual(lintDoc(collectionDoc({ collection_targeting })), []);
+ assert.deepEqual(lintDoc(collectionDoc({ collection_targeting, minimum_pairs: 2, require_cross_publisher_collection: true })), []);
+ }
+});
+test('collection operand rejects obsolete aliases and invalid closed values', () => {
+ for (const value of [{ mode: 'fixed' }, { collection_targeting: 'fixed', require_external_owner: true },
+ { collection_targeting: 'fixed', minimum_pairs: 0 }, { collection_targeting: 'fixed', minimum_pairs: '2' },
+ { collection_targeting: 'any', require_cross_publisher_collection: 'true' }, { collection_targeting: 'unknown' }]) {
+ assert.ok(lintDoc(collectionDoc(value)).some(v => v.rule === 'collection_selection_value'));
+ }
+});
diff --git a/tests/lint-storyboard-sample-request-schema.test.cjs b/tests/lint-storyboard-sample-request-schema.test.cjs
index 26247402cfb..d6c683c60ec 100644
--- a/tests/lint-storyboard-sample-request-schema.test.cjs
+++ b/tests/lint-storyboard-sample-request-schema.test.cjs
@@ -535,3 +535,41 @@ test('static build preparation requires the authored opt-in contract and rejects
assert.throws(() => normalizeStoryboardSampleRequests(storyboard, malformed, schema),
/Invalid creative build authoring fixture path/);
});
+
+test("whole collection witness substitutions satisfy minItems and composed required fields without excusing malformed literals", async () => {
+ const storyboard = yaml.load(
+ fs.readFileSync(
+ path.join(
+ STORYBOARD_DIR,
+ "protocols/media-buy/scenarios/collection_selection.yaml",
+ ),
+ "utf8",
+ ),
+ );
+ const step = storyboard.phases[0].steps[0];
+ assert.equal(
+ (
+ await validateStep({
+ schemaRef: step.schema_ref,
+ payload: step.sample_request,
+ })
+ ).ok,
+ true,
+ );
+ for (const collections of [[], [{ publisher_domain: "publisher.example" }]]) {
+ const malformed = structuredClone(step.sample_request);
+ malformed.packages[0].targeting_overlay.collection_selection.collections =
+ collections;
+ assert.equal(
+ (await validateStep({ schemaRef: step.schema_ref, payload: malformed }))
+ .ok,
+ false,
+ );
+ }
+});
+
+// Object-typed substitution synthesis — the lint change landed in this PR.
+// A substitution that lands at an object location (plain or inside a
+// discriminated oneOf) must produce a shape-valid placeholder or ajv will
+// reject every branch. Regression guard against refactors of `placeholderFor`
+// / `synthesizeObject` / `firstObjectBranch`.
diff --git a/tests/optional-product-collection-discovery.test.cjs b/tests/optional-product-collection-discovery.test.cjs
new file mode 100644
index 00000000000..3b43c0559e2
--- /dev/null
+++ b/tests/optional-product-collection-discovery.test.cjs
@@ -0,0 +1,116 @@
+"use strict";
+const test = require("node:test");
+const assert = require("node:assert/strict");
+const {
+ execute,
+ qualifying,
+ load,
+} = require("./optional-product-collections.test.cjs");
+
+for (const fault of [
+ "transport",
+ "protocol",
+ "schema",
+ "pagination",
+ "loop",
+ "invalid_version",
+])
+ for (const name of ["collection_selection", "owner_sold_channel_subset"])
+ for (const standalone of [false, true])
+ test(`discovery: ${name} ${fault} standalone=${standalone} cannot become an unsupported-feature skip`, async () => {
+ {
+ const { result, graded, calls } = await execute(name, {
+ standalone,
+ fault,
+ });
+ assert.ok(
+ graded.some((s) => s.disposition === "failed"),
+ JSON.stringify(result),
+ );
+ assert.equal(
+ calls.some((c) => c.task === "create_media_buy"),
+ false,
+ );
+ assert.equal(
+ result.coverage_gaps?.some(
+ (g) => g.reason === "fixture_unsatisfied",
+ ) ?? false,
+ false,
+ );
+ assert.ok(
+ result.fixture_resolutions.some((r) => r.status === "failed"),
+ );
+ }
+ });
+test("setup: a mandatory selected-entity capture failure stays failed with its producer evidence", async () => {
+ const doc = load("collection_selection");
+ doc.fixture_resolution.products[0].context_outputs.push({
+ key: "__adcp_fixture_required_missing",
+ path: "nonexistent_capture",
+ });
+ const { result, graded, calls } = await execute("collection_selection", {
+ doc,
+ });
+ assert.ok(
+ graded.some((s) => s.disposition === "failed"),
+ JSON.stringify(result),
+ );
+ assert.equal(
+ calls.some((c) => c.task === "create_media_buy"),
+ false,
+ );
+ assert.ok(result.fixture_resolutions.some((r) => r.status === "failed"));
+});
+for (const name of [
+ "collection_default_materialization",
+ "collection_selection_readback",
+])
+ test(`${name}: hosted response cap leaves oversize discovery failed`, async () => {
+ const { result, calls } = await execute(name, {
+ hosted: true,
+ responsePadding: 1024 * 1024 + 128,
+ });
+ assert.equal(result.overall_passed, false, JSON.stringify(result));
+ assert.equal(
+ calls.some((c) => c.task === "create_media_buy"),
+ false,
+ );
+ assert.equal(
+ result.coverage_gaps?.some((g) => g.reason === "fixture_unsatisfied") ??
+ false,
+ false,
+ );
+ });
+
+for (const malformed of [
+ "bulk_product",
+ "plural_property",
+ "wrong_flag",
+ "empty_ids",
+])
+ test(`invalid catalog: ${malformed} fails schema instead of fixture_unsatisfied`, async () => {
+ const offer = qualifying("owner_sold_channel_carriage");
+ if (malformed === "bulk_product")
+ offer.collections = [{ publisher_domain: "studio.example" }];
+ if (malformed === "plural_property")
+ offer.publisher_properties[0].publisher_domains = ["forbidden.example"];
+ if (malformed === "wrong_flag") offer.collection_targeting_allowed = null;
+ if (malformed === "empty_ids") offer.collections[0].collection_ids = [];
+ const { result, graded, calls } = await execute(
+ "owner_sold_channel_carriage",
+ { catalog: [offer] },
+ );
+ assert.ok(
+ graded.some((s) => s.disposition === "failed"),
+ JSON.stringify(result),
+ );
+ assert.equal(
+ result.coverage_gaps?.some((g) => g.reason === "fixture_unsatisfied") ??
+ false,
+ false,
+ );
+ assert.equal(
+ calls.some((c) => c.task === "create_media_buy"),
+ false,
+ );
+ });
diff --git a/tests/optional-product-collection-echo.test.cjs b/tests/optional-product-collection-echo.test.cjs
new file mode 100644
index 00000000000..3edbb6dfe23
--- /dev/null
+++ b/tests/optional-product-collection-echo.test.cjs
@@ -0,0 +1,63 @@
+"use strict";
+const test = require("node:test");
+const assert = require("node:assert/strict");
+const { execute, CORE } = require("./optional-product-collections.test.cjs");
+
+for (const broken of [
+ "domain",
+ "missing_echo",
+ "drop",
+ "duplicate_echo",
+ "extra_package",
+ "reject_all",
+ "wrong_product",
+ "missing_product",
+])
+ test(`advertised broken support: ${broken} remains a raw failed grade`, async () => {
+ for (const name of CORE) {
+ const { result, graded } = await execute(name, { broken });
+ assert.equal(result.overall_passed, false, JSON.stringify(result));
+ assert.ok(graded.some((s) => s.disposition === "failed"));
+ assert.equal(
+ result.coverage_gaps?.some((g) => g.reason === "fixture_unsatisfied") ??
+ false,
+ false,
+ );
+ }
+ });
+for (const name of ["collection_selection", "owner_sold_channel_subset"])
+ test(`${name}: silently widening a strict subset fails exact pair-set grading`, async () => {
+ const { result } = await execute(name, { broken: "widen" });
+ assert.equal(result.overall_passed, false);
+ const failures = result.phases
+ .flatMap((p) => p.steps.flatMap((s) => s.validations ?? []))
+ .filter((v) => v.check === "collection_set_equals_context" && !v.passed);
+ assert.ok(failures.length);
+ for (const v of failures) {
+ for (const key of [
+ "check",
+ "passed",
+ "description",
+ "error",
+ "json_pointer",
+ "expected",
+ "actual",
+ "request",
+ "response",
+ "schema_id",
+ "schema_url",
+ ])
+ assert.ok(Object.hasOwn(v, key), key);
+ // SDK16.3 omits authored context_key (SDK#3396). The unchanged normative
+ // assertion and raw failed baseline are preserved separately; this case
+ // qualifies failed grading and the remaining actual wire evidence only.
+ assert.equal(v.schema_id, null);
+ assert.equal(v.schema_url, null);
+ assert.equal(
+ v.json_pointer,
+ "/packages/0/targeting_overlay/collection_selection/collections",
+ );
+ assert.equal(v.request.transport, "mcp");
+ assert.equal(v.response.transport, "mcp");
+ }
+ });
diff --git a/tests/optional-product-collection-history.test.cjs b/tests/optional-product-collection-history.test.cjs
new file mode 100644
index 00000000000..ebdab53000f
--- /dev/null
+++ b/tests/optional-product-collection-history.test.cjs
@@ -0,0 +1,207 @@
+"use strict";
+const test = require("node:test");
+const assert = require("node:assert/strict");
+const { execute, READ } = require("./optional-product-collections.test.cjs");
+
+for (const name of READ) {
+ for (const mode of [
+ "valid",
+ "probe_error",
+ "schema_error",
+ "malformed_modes",
+ "fail_once",
+ ]) {
+ test(`${name}: ${mode} cannot bypass any required committed comparand`, async () => {
+ for (const key of [
+ "collection_media_buy_id",
+ "collection_package_id",
+ "collection_committed_product_id",
+ "collection_committed_selectors",
+ "catalog_collection_commitment_proof",
+ ]) {
+ const { error, calls, created } = await execute(name, {
+ standalone: true,
+ stepId: "read_selection",
+ readProfileMode: mode,
+ expectLocalError: true,
+ priorContextTransform: (context) => {
+ delete context[key];
+ },
+ });
+ assert.equal(
+ created.passed,
+ true,
+ "real prior create is the positive control",
+ );
+ assert.match(
+ error?.message ?? "",
+ /requires (trusted prior|verified successful-create)/,
+ key,
+ );
+ assert.equal(
+ calls.some((c) => c.task === "get_media_buys"),
+ false,
+ key,
+ );
+ }
+ });
+ test(`${name}: ${mode} retains native execution with verified history`, async () => {
+ const { result, calls, error } = await execute(name, {
+ standalone: true,
+ stepId: "read_selection",
+ readProfileMode: mode,
+ expectLocalError: true,
+ });
+ assert.equal(error, undefined);
+ if (mode === "valid" || mode === "fail_once") {
+ assert.equal(result.passed, true, JSON.stringify(result));
+ assert.equal(
+ calls.some((c) => c.task === "get_media_buys"),
+ true,
+ );
+ } else {
+ // Delegate malformed/failed supplied discovery unchanged. It may grade
+ // discovery failure or recover; never fabricate a local seller grade.
+ assert.ok(result && typeof result.passed === "boolean");
+ }
+ });
+ }
+ test(`${name}: actual failed-create captures cannot authorize a standalone read`, async () => {
+ const { error, calls, created } = await execute(name, {
+ standalone: true,
+ stepId: "read_selection",
+ broken: "domain",
+ continueFailedCreate: true,
+ expectLocalError: true,
+ });
+ assert.equal(created.passed, false);
+ assert.ok(
+ created.validations.some(
+ (v) => v.check === "collection_set_equals_context" && !v.passed,
+ ),
+ );
+ assert.ok(
+ created.context.collection_media_buy_id,
+ "retain raw captured evidence",
+ );
+ assert.ok(created.response, "retain ordinary failed response evidence");
+ assert.equal(
+ created.context.catalog_collection_commitment_proof,
+ undefined,
+ );
+ assert.match(error?.message ?? "", /requires verified successful-create/);
+ assert.equal(
+ calls.some((c) => c.task === "get_media_buys"),
+ false,
+ );
+ });
+ test(`${name}: retargeting a valid prior commitment fails before read`, async () => {
+ for (const key of [
+ "collection_media_buy_id",
+ "collection_package_id",
+ "collection_committed_product_id",
+ "collection_committed_selectors",
+ "catalog_collection_commitment_proof",
+ ]) {
+ const { error, calls } = await execute(name, {
+ standalone: true,
+ stepId: "read_selection",
+ expectLocalError: true,
+ priorContextTransform: (context) => {
+ context[key] = key.endsWith("selectors")
+ ? [
+ {
+ publisher_domain: "wrong.example",
+ collection_ids: ["foreign"],
+ },
+ ]
+ : key.endsWith("proof")
+ ? "0".repeat(64)
+ : "foreign";
+ },
+ });
+ assert.match(error?.message ?? "", /requires verified successful-create/);
+ assert.equal(
+ calls.some((c) => c.task === "get_media_buys"),
+ false,
+ );
+ }
+ });
+ test(`${name}: ordinary caller booleans and complete unissued context are not provenance`, async () => {
+ const { error, calls } = await execute(name, {
+ standalone: true,
+ stepId: "read_selection",
+ skipPriorCreate: true,
+ expectLocalError: true,
+ context: {
+ collection_media_buy_id: "buy",
+ collection_package_id: "pkg",
+ collection_committed_product_id: "native-product",
+ collection_committed_selectors: [
+ { publisher_domain: "studio.example", collection_ids: ["episode-a"] },
+ ],
+ create_passed: true,
+ catalog_collection_commitment_proof: "0".repeat(64),
+ },
+ });
+ assert.match(error?.message ?? "", /requires verified successful-create/);
+ assert.equal(calls.length, 0);
+ });
+}
+
+for (const name of READ) {
+ for (const changed of ["seller", "storyboard", "account", "version"]) {
+ test(`${name}: a commitment cannot be replayed into a different ${changed}`, async () => {
+ const { error, calls } = await execute(name, {
+ standalone: true,
+ stepId: "read_selection",
+ expectLocalError: true,
+ ...(changed === "seller" && {
+ readAgentUrl: "https://other-controlled-seller.example/mcp",
+ }),
+ priorRunTransform: ({ doc, options }) => {
+ if (changed === "storyboard") doc.title += " changed";
+ if (changed === "account")
+ doc.context.sandbox_account.operator = "different-operator.example";
+ if (changed === "version") options.adcpVersion = "3.2.4";
+ },
+ });
+ assert.match(error?.message ?? "", /requires verified successful-create/);
+ assert.equal(
+ calls.some((c) => c.task === "get_media_buys"),
+ false,
+ );
+ });
+ }
+}
+
+for (const name of READ) {
+ test(`${name}: protected caller metadata retains the SDK typed denial and private audit`, async () => {
+ for (const map of [
+ "context",
+ "context_provenance",
+ "response_derived_not_applicable_context_keys",
+ ]) {
+ const audits = [];
+ const { error, http } = await execute(name, {
+ standalone: true,
+ stepId: "read_selection",
+ skipPriorCreate: true,
+ expectLocalError: true,
+ overrides: {
+ [map]: { __adcp_fixture_forged: "controlled" },
+ prepared_fixtures_audit_sink: (record) => {
+ audits.push(record);
+ },
+ },
+ });
+ // Production imports public ESM; CJS/ESM class identities are distinct.
+ const { PreparedFixturesDeniedError } = await import("@adcp/sdk/testing");
+ assert.ok(error instanceof PreparedFixturesDeniedError, map);
+ assert.equal(error.code, "PREPARED_FIXTURES_DENIED");
+ assert.equal(error.refusalCode, "unauthorized_metadata");
+ assert.equal(http.length, 0);
+ assert.equal(audits.length, 1);
+ }
+ });
+}
diff --git a/tests/optional-product-collection-price.test.cjs b/tests/optional-product-collection-price.test.cjs
new file mode 100644
index 00000000000..167184864c9
--- /dev/null
+++ b/tests/optional-product-collection-price.test.cjs
@@ -0,0 +1,87 @@
+"use strict";
+const test = require("node:test");
+const assert = require("node:assert/strict");
+const {
+ execute,
+ qualifying,
+ ALL,
+} = require("./optional-product-collections.test.cjs");
+
+for (const minimum of [undefined, 0, 5000])
+ test(`same graph: later qualifying product and scoped price preserve minimum ${String(minimum)}`, async () => {
+ for (const name of ALL) {
+ const noPrice = qualifying(name, {
+ prices: [
+ {
+ pricing_option_id: "auction-only",
+ pricing_model: "cpm",
+ currency: "USD",
+ floor_price: 900,
+ },
+ ],
+ });
+ noPrice.product_id = "a-earlier-unusable";
+ const selected = qualifying(name, { minimum });
+ selected.product_id = "z-later-qualified";
+ selected.pricing_options.unshift({
+ pricing_option_id: "unrelated-auction",
+ pricing_model: "cpm",
+ currency: "USD",
+ floor_price: 999,
+ });
+ const { result, calls } = await execute(name, {
+ pages: [[noPrice], [selected]],
+ context: {
+ products: [noPrice],
+ catalog_collection_budget: 1,
+ catalog_collection_full_selectors: [
+ { publisher_domain: "forged.example", collection_ids: ["forged"] },
+ ],
+ },
+ });
+ assert.equal(result.overall_passed, true, JSON.stringify(result));
+ assert.deepEqual(
+ calls
+ .filter((c) => c.task === "get_products")
+ .map((c) => c.request.pagination?.cursor),
+ [undefined, "1"],
+ );
+ const buy = calls.find((c) => c.task === "create_media_buy").request;
+ assert.equal(buy.packages[0].product_id, selected.product_id);
+ assert.equal(buy.packages[0].pricing_option_id, "native-product-fixed");
+ assert.equal(buy.packages[0].budget, Math.max(1000, minimum ?? 0));
+ }
+ });
+
+for (const minimum of [undefined, 0, 5000])
+ test(`selected snapshot faithfully exposes optional minimum ${String(minimum)}`, async () => {
+ const offer = qualifying("collection_selection", { minimum });
+ const { prepared } = await execute("collection_selection", {
+ catalog: [offer],
+ prepareOnly: true,
+ });
+ assert.equal(prepared.disposition.status, "resolved");
+ const price = prepared.captures.__adcp_fixture_collection_price;
+ assert.equal(
+ Object.hasOwn(price, "min_spend_per_package"),
+ minimum !== undefined,
+ );
+ assert.equal(price.min_spend_per_package, minimum);
+ assert.equal(
+ price.pricing_option_id,
+ offer.pricing_options[0].pricing_option_id,
+ );
+ assert.equal(price.currency, "EUR");
+ assert.equal(price.fixed_price, 17);
+ // Public diagnostic views are detached copies; mutation cannot change the
+ // SDK-owned selected snapshot used by consumers or future capture reads.
+ price.fixed_price = 999;
+ price.min_spend_per_package = 999;
+ const fresh = prepared.captures.__adcp_fixture_collection_price;
+ assert.equal(fresh.fixed_price, 17);
+ assert.equal(
+ Object.hasOwn(fresh, "min_spend_per_package"),
+ minimum !== undefined,
+ );
+ assert.equal(fresh.min_spend_per_package, minimum);
+ });
diff --git a/tests/optional-product-collection-rejections.test.cjs b/tests/optional-product-collection-rejections.test.cjs
new file mode 100644
index 00000000000..0f2cc14c5fd
--- /dev/null
+++ b/tests/optional-product-collection-rejections.test.cjs
@@ -0,0 +1,106 @@
+"use strict";
+const test = require("node:test");
+const assert = require("node:assert/strict");
+const { execute, READ } = require("./optional-product-collections.test.cjs");
+
+for (const broken of ["accept_invalid", "wrong_error"])
+ test(`negative assertions: ${broken} cannot pass by rejecting everything or accepting invalid selectors`, async () => {
+ for (const name of [
+ "collection_selection",
+ "fixed_bundle_enforcement",
+ "owner_sold_channel_carriage",
+ ]) {
+ const { result } = await execute(name, { broken });
+ assert.equal(result.overall_passed, false, JSON.stringify(result));
+ assert.equal(
+ result.phases[0].steps[0].passed,
+ true,
+ "same-product affirmative control ran before negatives",
+ );
+ assert.ok(
+ result.phases
+ .flatMap((p) => p.steps)
+ .some((s) =>
+ s.validations?.some((v) => v.check === "error_code" && !v.passed),
+ ),
+ );
+ }
+ });
+for (const name of [
+ "collection_default_materialization",
+ "collection_default_materialization_readback",
+])
+ test(`${name}: default must materialize concrete selectors`, async () => {
+ const { result } = await execute(name, {
+ broken: "default_unmaterialized",
+ });
+ assert.equal(result.overall_passed, false);
+ });
+for (const broken of [
+ "read_identity",
+ "read_product",
+ "read_missing_product",
+ "read_package",
+ "read_widen",
+ "read_missing",
+])
+ test(`persisted readback: ${broken} fails full and standalone when reader is advertised`, async () => {
+ for (const name of READ)
+ for (const standalone of [false, true]) {
+ const { result, graded } = await execute(name, {
+ standalone,
+ stepId: "read_selection",
+ broken,
+ });
+ assert.equal(
+ standalone ? result.passed : result.overall_passed,
+ false,
+ JSON.stringify(result),
+ );
+ assert.ok(
+ graded.some(
+ (s) => s.step_id === "read_selection" && s.disposition === "failed",
+ ),
+ );
+ }
+ });
+
+for (const name of READ)
+ test(`${name}: standalone read requires every prior identity and concrete committed set`, async () => {
+ const complete = {
+ collection_media_buy_id: "prior-buy",
+ collection_package_id: "prior-package",
+ collection_committed_product_id: "prior-product",
+ collection_committed_selectors: [
+ { publisher_domain: "studio.example", collection_ids: ["episode-a"] },
+ ],
+ };
+ for (const missing of Object.keys(complete)) {
+ const context = structuredClone(complete);
+ delete context[missing];
+ const { error, http } = await execute(name, {
+ standalone: true,
+ stepId: "read_selection",
+ context,
+ skipPriorCreate: true,
+ expectLocalError: true,
+ });
+ assert.match(
+ error.message,
+ /requires trusted prior buy, package, product and concrete committed-selector context/,
+ );
+ assert.equal(http.length, 0);
+ }
+ });
+
+for (const name of READ)
+ test(`${name}: a failed create never reaches persisted readback in a full run`, async () => {
+ for (const broken of ["domain", "wrong_product"]) {
+ const { result, calls } = await execute(name, { broken });
+ assert.equal(result.overall_passed, false, JSON.stringify(result));
+ assert.equal(
+ calls.some((c) => c.task === "get_media_buys"),
+ false,
+ );
+ }
+ });
diff --git a/tests/optional-product-collection-scope.test.cjs b/tests/optional-product-collection-scope.test.cjs
new file mode 100644
index 00000000000..a900b7a7a87
--- /dev/null
+++ b/tests/optional-product-collection-scope.test.cjs
@@ -0,0 +1,251 @@
+"use strict";
+const test = require("node:test");
+const assert = require("node:assert/strict");
+const {
+ execute,
+ product,
+ qualifying,
+ READ,
+ ALL,
+ pairs,
+} = require("./optional-product-collections.test.cjs");
+
+for (const [label, offer, applicable] of [
+ [
+ "one fixed omitted pair",
+ product("one-fixed", { flag: "omitted", pairs: 1 }),
+ [
+ "collection_default_materialization",
+ "fixed_bundle_restatement",
+ "collection_default_materialization_readback",
+ ],
+ ],
+ [
+ "one selectable pair",
+ product("one-selectable", { pairs: 1 }),
+ [
+ "collection_default_materialization",
+ "collection_default_materialization_readback",
+ ],
+ ],
+ [
+ "one cross-publisher fixed pair",
+ product("one-cross", { flag: "omitted", pairs: 1, cross: true }),
+ [
+ "collection_default_materialization",
+ "fixed_bundle_restatement",
+ "owner_sold_channel_carriage",
+ "collection_default_materialization_readback",
+ ],
+ ],
+ [
+ "two fixed false pairs",
+ product("two-fixed", { flag: false }),
+ [
+ "collection_default_materialization",
+ "fixed_bundle_enforcement",
+ "fixed_bundle_restatement",
+ "collection_default_materialization_readback",
+ ],
+ ],
+])
+ test(`independence: ${label} cannot be suppressed by a missing companion fixture`, async () => {
+ for (const name of ALL) {
+ const { result, calls } = await execute(name, {
+ catalog: [structuredClone(offer)],
+ });
+ assert.equal(result.overall_passed, true, JSON.stringify(result));
+ assert.equal(
+ calls.some((c) => c.task === "create_media_buy"),
+ applicable.includes(name),
+ name,
+ );
+ assert.equal(
+ result.coverage_gaps?.some((g) => g.reason === "fixture_unsatisfied") ??
+ false,
+ !applicable.includes(name),
+ name,
+ );
+ }
+ });
+test("cross-publisher: one local-first pair and one retained external pair qualify independently", async () => {
+ const offer = product("mixed", {
+ collections: [
+ { publisher_domain: "a-local.example", collection_ids: ["same"] },
+ { publisher_domain: "z-external.example", collection_ids: ["same"] },
+ ],
+ });
+ offer.publisher_properties = [
+ { publisher_domain: "a-local.example", selection_type: "all" },
+ { publisher_domain: "host-two.example", selection_type: "all" },
+ ];
+ for (const name of [
+ "owner_sold_channel_carriage",
+ "owner_sold_channel_subset",
+ ]) {
+ const { result, calls } = await execute(name, {
+ catalog: [structuredClone(offer)],
+ });
+ assert.equal(result.overall_passed, true, JSON.stringify(result));
+ const buys = calls.filter((c) => c.task === "create_media_buy");
+ if (name.endsWith("subset"))
+ assert.deepEqual(
+ buys[0].request.packages[0].targeting_overlay.collection_selection
+ .collections,
+ [{ publisher_domain: "z-external.example", collection_ids: ["same"] }],
+ );
+ else {
+ assert.deepEqual(
+ buys[1].request.packages[0].targeting_overlay.collection_selection
+ .collections,
+ [{ publisher_domain: "z-external.example" }],
+ );
+ assert.equal(
+ buys[2].request.packages[0].targeting_overlay.collection_selection
+ .collections[0].publisher_domain,
+ "z-external.example",
+ );
+ assert.equal(
+ buys[3].request.packages[0].targeting_overlay.collection_selection
+ .collections[0].collection_ids[0],
+ "same",
+ );
+ }
+ }
+});
+test("cross-publisher: namespaces appearing anywhere in product properties are not external", async () => {
+ const offer = product("all-local", {
+ collections: [
+ { publisher_domain: "first.example", collection_ids: ["x"] },
+ { publisher_domain: "second.example", collection_ids: ["y"] },
+ ],
+ });
+ offer.publisher_properties = [
+ { publisher_domain: "first.example", selection_type: "all" },
+ { publisher_domain: "second.example", selection_type: "all" },
+ ];
+ for (const name of [
+ "owner_sold_channel_carriage",
+ "owner_sold_channel_subset",
+ ]) {
+ const { result, calls } = await execute(name, { catalog: [offer] });
+ assert.ok(
+ result.coverage_gaps.some((g) => g.reason === "fixture_unsatisfied"),
+ );
+ assert.equal(
+ calls.some((c) => c.task === "create_media_buy"),
+ false,
+ );
+ }
+});
+test("set semantics: legal catalog redundancy, empty and Unicode IDs stay concrete without duplicate wire entries", async () => {
+ const collections = [
+ { publisher_domain: "studio.example", collection_ids: ["", "é", "é"] },
+ { publisher_domain: "studio.example", collection_ids: ["", "é", "é"] },
+ { publisher_domain: "other.example", collection_ids: ["é"] },
+ ];
+ for (const name of [
+ "collection_default_materialization",
+ "fixed_bundle_restatement",
+ "collection_selection",
+ ]) {
+ const offer = qualifying(name, { collections });
+ const { result, calls } = await execute(name, { catalog: [offer] });
+ assert.equal(result.overall_passed, true, JSON.stringify(result));
+ if (name === "fixed_bundle_restatement") {
+ const sent = calls.find((c) => c.task === "create_media_buy").request
+ .packages[0].targeting_overlay.collection_selection.collections;
+ assert.equal(sent.length, 3);
+ assert.equal(
+ new Set(sent.map((s) => JSON.stringify(s))).size,
+ sent.length,
+ );
+ assert.deepEqual(pairs(sent), pairs(collections));
+ }
+ }
+});
+
+test("negative witnesses collision-check the COMPLETE selected product rather than a reserved-looking prefix", async () => {
+ const { evaluateCollectionSelection } = require("@adcp/sdk/testing");
+ const offer = product("collision-product", { cross: true });
+ const initial = evaluateCollectionSelection(offer, {
+ collection_targeting: "any",
+ minimum_pairs: 1,
+ require_cross_publisher_collection: true,
+ }).witness;
+ offer.collections[0].collection_ids.push(
+ initial.undeclared_collection_id_selectors[0].collection_ids[0],
+ );
+ offer.collections.push(
+ structuredClone(initial.undeclared_publisher_domain_selectors[0]),
+ );
+ const { result, calls } = await execute("owner_sold_channel_carriage", {
+ catalog: [offer],
+ });
+ assert.equal(result.overall_passed, true, JSON.stringify(result));
+ const negative = calls.filter((c) => c.task === "create_media_buy").slice(2);
+ const declared = pairs(offer.collections);
+ for (const call of negative)
+ for (const pair of pairs(
+ call.request.packages[0].targeting_overlay.collection_selection
+ .collections,
+ ))
+ assert.equal(declared.has(pair), false);
+ const sentDomain =
+ negative[1].request.packages[0].targeting_overlay.collection_selection
+ .collections[0].publisher_domain;
+ assert.equal(
+ offer.collections.some((s) => s.publisher_domain === sentDomain),
+ false,
+ );
+});
+
+test("root applicability: brief-only seller remains excluded in full and standalone without fixture contact", async () => {
+ for (const name of ALL)
+ for (const standalone of [false, true]) {
+ const { result, calls, graded } = await execute(name, {
+ standalone,
+ buyingModes: ["brief"],
+ });
+ assert.equal(calls.length, 0, JSON.stringify(calls));
+ assert.ok(
+ graded.every((s) => s.disposition === "not_applicable"),
+ JSON.stringify(result),
+ );
+ }
+});
+
+for (const name of READ)
+ test(`${name}: stale caller tool caches cannot override the observed reader gate`, async () => {
+ for (const standalone of [false, true]) {
+ const absent = await execute(name, {
+ standalone,
+ reader: false,
+ stepId: "read_selection",
+ overrides: {
+ agentTools: [
+ "get_media_buys",
+ "get_products",
+ "create_media_buy",
+ "comply_test_controller",
+ ],
+ },
+ });
+ assert.equal(absent.http.length, 0);
+ assert.equal(absent.calls.length, 0);
+ assert.ok(
+ absent.graded.every((s) => s.disposition === "not_applicable"),
+ JSON.stringify(absent.result),
+ );
+ const present = await execute(name, {
+ standalone,
+ stepId: "read_selection",
+ overrides: { agentTools: [] },
+ });
+ assert.equal(
+ standalone ? present.result.passed : present.result.overall_passed,
+ true,
+ JSON.stringify(present.result),
+ );
+ }
+ });
diff --git a/tests/optional-product-collections.test.cjs b/tests/optional-product-collections.test.cjs
new file mode 100644
index 00000000000..2dc1cae6fa0
--- /dev/null
+++ b/tests/optional-product-collections.test.cjs
@@ -0,0 +1,628 @@
+"use strict";
+
+// Executes the actual authored storyboards through the production caller and
+// public SDK MCP transport. All seller data/credentials are controlled fixtures.
+const test = require("node:test");
+const assert = require("node:assert/strict");
+const path = require("node:path");
+const {
+ loadStoryboardFile,
+ prepareStoryboardFixtures,
+} = require("@adcp/sdk/testing");
+const {
+ runCatalogReportingStoryboard: runStoryboard,
+ runCatalogReportingStoryboardStep: runStoryboardStep,
+ withHostedReportingBudget,
+} = require("../server/src/compliance/catalog-reporting-runner.ts");
+const {
+ classifyComplianceStep,
+} = require("../server/src/compliance/step-disposition.ts");
+const SOURCE = path.join(
+ __dirname,
+ "../static/compliance/source/protocols/media-buy/scenarios",
+);
+const CORE = [
+ "collection_selection",
+ "collection_default_materialization",
+ "fixed_bundle_enforcement",
+ "fixed_bundle_restatement",
+ "owner_sold_channel_carriage",
+ "owner_sold_channel_subset",
+];
+const READ = [
+ "collection_selection_readback",
+ "collection_default_materialization_readback",
+];
+const ALL = [...CORE, ...READ];
+const URL = "https://collection-regression.example/mcp";
+const load = (name) => loadStoryboardFile(path.join(SOURCE, `${name}.yaml`));
+
+function product(
+ id = "native-product",
+ { flag = true, cross = false, pairs = 2, collections, minimum, prices } = {},
+) {
+ return {
+ product_id: id,
+ name: "StreamHaus collection offer",
+ description: "Native sandbox product",
+ delivery_type: "non_guaranteed",
+ channels: ["olv"],
+ publisher_properties: [
+ {
+ publisher_domain: cross ? "host.example" : "studio.example",
+ selection_type: "all",
+ },
+ ],
+ format_options: [
+ {
+ format_option_id: "native-image",
+ format_kind: "image",
+ params: { width: 300, height: 250 },
+ },
+ ],
+ reporting_capabilities: {
+ available_reporting_frequencies: ["daily"],
+ expected_delay_minutes: 0,
+ timezone: "UTC",
+ supports_webhooks: false,
+ available_metrics: ["impressions", "clicks", "spend"],
+ date_range_support: "date_range",
+ },
+ collections: collections ?? [
+ {
+ publisher_domain: "studio.example",
+ collection_ids: ["episode-a", "episode-b"].slice(0, pairs),
+ },
+ ],
+ ...(flag === "omitted" ? {} : { collection_targeting_allowed: flag }),
+ pricing_options: prices ?? [
+ {
+ pricing_option_id: `${id}-fixed`,
+ pricing_model: "cpm",
+ currency: "EUR",
+ fixed_price: 17,
+ ...(minimum === undefined ? {} : { min_spend_per_package: minimum }),
+ },
+ ],
+ };
+}
+function qualifying(name, options = {}) {
+ const fixed = name.startsWith("fixed_bundle");
+ const cross = name.startsWith("owner_sold");
+ return product("native-product", { flag: !fixed, cross, ...options });
+}
+function pairs(selectors) {
+ return new Set(
+ selectors.flatMap((s) =>
+ (s.collection_ids ?? []).map((id) =>
+ JSON.stringify([s.publisher_domain, id]),
+ ),
+ ),
+ );
+}
+function concrete(selectors) {
+ const domains = new Map();
+ for (const selector of selectors)
+ for (const id of selector.collection_ids ?? []) {
+ if (!domains.has(selector.publisher_domain))
+ domains.set(selector.publisher_domain, new Set());
+ domains.get(selector.publisher_domain).add(id);
+ }
+ return [...domains].reverse().map(([publisher_domain, ids]) => ({
+ publisher_domain,
+ collection_ids: [...ids].reverse(),
+ }));
+}
+
+async function execute(
+ name,
+ {
+ catalog = [qualifying(name)],
+ pages,
+ fault,
+ broken,
+ reader = true,
+ standalone = false,
+ stepId = "create_selection",
+ context,
+ overrides = {},
+ noProfile = false,
+ hosted = false,
+ responsePadding = 0,
+ prepareOnly = false,
+ buyingModes = ["wholesale"],
+ doc = load(name),
+ mutateAfterCreate,
+ omitCreateLifecycle = false,
+ skipPriorCreate = false,
+ expectLocalError = false,
+ continueFailedCreate = false,
+ priorContextTransform,
+ readProfileMode,
+ readAgentUrl = URL,
+ priorRunTransform,
+ scenarioOnly = false,
+ } = {},
+) {
+ const calls = [],
+ wire = [],
+ http = [],
+ buys = new Map();
+ const tools = [
+ "get_adcp_capabilities",
+ "get_products",
+ "create_media_buy",
+ "comply_test_controller",
+ ...(reader ? ["get_media_buys"] : []),
+ ];
+ const caps = {
+ status: "completed",
+ adcp: {
+ major_versions: [3],
+ supported_versions: ["3.2"],
+ idempotency: { supported: false },
+ },
+ supported_protocols: ["media_buy"],
+ media_buy: { buying_modes: buyingModes },
+ compliance_testing: { scenarios: ["seed_account"] },
+ };
+ let buyIndex = 0;
+ let failReadCapabilities = false;
+ const trustedFetchFn = async (_url, init = {}) => {
+ http.push({ method: init.method ?? "GET", body: init.body });
+ if (!init.body)
+ return new Response(null, {
+ status: init.method === "DELETE" ? 202 : 405,
+ });
+ const msg = JSON.parse(init.body);
+ wire.push(msg.method);
+ const reply = (result) =>
+ new Response(JSON.stringify({ jsonrpc: "2.0", id: msg.id, result }), {
+ headers: { "content-type": "application/json" },
+ });
+ if (msg.method === "server/discover")
+ return new Response(
+ JSON.stringify({
+ jsonrpc: "2.0",
+ id: msg.id,
+ error: { code: -32601, message: "Not supported" },
+ }),
+ { headers: { "content-type": "application/json" } },
+ );
+ if (msg.method === "initialize")
+ return reply({
+ protocolVersion: "2025-03-26",
+ capabilities: { tools: {} },
+ serverInfo: { name: "collection-regression", version: "1" },
+ });
+ if (msg.method.startsWith("notifications/"))
+ return new Response(null, { status: 202 });
+ if (msg.method === "tools/list")
+ return reply({
+ tools: tools.map((name) => ({
+ name,
+ description: name,
+ inputSchema: { type: "object", additionalProperties: true },
+ })),
+ });
+ assert.equal(msg.method, "tools/call");
+ const { name: task, arguments: request } = msg.params;
+ calls.push({ task, request: structuredClone(request) });
+ let data;
+ if (task === "get_adcp_capabilities") {
+ if (failReadCapabilities) {
+ failReadCapabilities = false;
+ return new Response("Controlled first read capability failure", {
+ status: 503,
+ });
+ }
+ data = caps;
+ } else if (task === "comply_test_controller") {
+ assert.equal(request.account.sandbox, true);
+ assert.ok(
+ ["list_scenarios", "seed_account"].includes(request.scenario),
+ "feature support cannot be manufactured by seeding",
+ );
+ data =
+ request.scenario === "list_scenarios"
+ ? { success: true, scenarios: ["seed_account"] }
+ : { success: true };
+ } else if (task === "get_products") {
+ assert.equal(request.buying_mode, "wholesale");
+ assert.deepEqual(request.account, doc.context.sandbox_account);
+ if (fault === "transport")
+ throw new Error("Controlled catalog transport failure");
+ if (fault === "protocol")
+ data = {
+ status: "error",
+ errors: [
+ {
+ code: "INVALID_REQUEST",
+ message: "Controlled wholesale refusal",
+ },
+ ],
+ };
+ else {
+ const index = request.pagination?.cursor
+ ? Number(request.pagination.cursor)
+ : 0;
+ data = {
+ status: "completed",
+ products:
+ fault === "schema"
+ ? [{ product_id: "malformed" }]
+ : pages
+ ? pages[index]
+ : catalog,
+ cache_scope: "account",
+ wholesale_feed_version: "catalog-v1",
+ ...(fault === "invalid_version" && { adcp_version: "not-a-release" }),
+ ...(pages && {
+ pagination: {
+ has_more: index < pages.length - 1,
+ ...(index < pages.length - 1 && { cursor: String(index + 1) }),
+ },
+ }),
+ ...(fault === "pagination" && { pagination: { has_more: true } }),
+ ...(fault === "loop" && {
+ pagination: { has_more: true, cursor: "0" },
+ }),
+ };
+ }
+ } else if (task === "create_media_buy") {
+ assert.deepEqual(request.account, doc.context.sandbox_account);
+ assert.deepEqual(request.brand, doc.context.sandbox_brand);
+ assert.equal(Object.hasOwn(request, "currency"), false);
+ const pkg = request.packages[0];
+ const selected = (pages ? pages.flat() : catalog).find(
+ (p) => p.product_id === pkg.product_id,
+ );
+ assert.ok(selected, "the request uses a real selected seller product");
+ const price = selected.pricing_options.find(
+ (p) => p.pricing_option_id === pkg.pricing_option_id,
+ );
+ assert.ok(
+ price && price.fixed_price !== undefined,
+ "the price belongs to that SAME selected parent",
+ );
+ assert.equal(pkg.bid_price, undefined);
+ assert.equal(
+ pkg.budget,
+ Math.max(1000, price.min_spend_per_package ?? 0),
+ );
+ const selection = pkg.targeting_overlay.collection_selection;
+ const full = pairs(selected.collections);
+ const wanted =
+ selection.mode === "default" ? full : pairs(selection.collections);
+ let error;
+ if (
+ selection.mode === "selected" &&
+ selection.collections.some((s) => !Array.isArray(s.collection_ids))
+ )
+ error = "INVALID_REQUEST";
+ else if ([...wanted].some((p) => !full.has(p)))
+ error = "REFERENCE_NOT_FOUND";
+ else if (
+ selected.collection_targeting_allowed !== true &&
+ wanted.size !== full.size
+ )
+ error = "UNSUPPORTED_FEATURE";
+ if (broken === "reject_all") error = "UNSUPPORTED_FEATURE";
+ if (error && broken !== "accept_invalid") {
+ data = {
+ status: "error",
+ errors: [
+ {
+ code: broken === "wrong_error" ? "INVALID_REQUEST" : error,
+ message: "Controlled selector refusal",
+ },
+ ],
+ };
+ } else {
+ const selectedSelectors =
+ selection.mode === "default" || broken === "widen"
+ ? concrete(selected.collections)
+ : concrete(selection.collections);
+ let echo = { mode: "selected", collections: selectedSelectors };
+ if (broken === "domain")
+ echo.collections[0].publisher_domain = "wrong-namespace.example";
+ if (broken === "drop") echo.collections = [];
+ if (broken === "duplicate_echo")
+ echo.collections.push(structuredClone(echo.collections[0]));
+ if (broken === "default_unmaterialized" && selection.mode === "default")
+ echo = { mode: "default" };
+ const media_buy_id = `seller-buy-${++buyIndex}`,
+ package_id = `seller-package-${buyIndex}`;
+ const pack = {
+ package_id,
+ product_id: selected.product_id,
+ pricing_option_id: price.pricing_option_id,
+ ...(broken === "missing_echo"
+ ? {}
+ : { targeting_overlay: { collection_selection: echo } }),
+ };
+ data = {
+ status: "completed",
+ ...(omitCreateLifecycle ? {} : { media_buy_status: "active" }),
+ media_buy_id,
+ confirmed_at: "2026-10-23T00:00:00Z",
+ revision: 1,
+ packages: [pack],
+ };
+ if (broken === "wrong_product")
+ data.packages[0].product_id = "foreign-product";
+ if (broken === "missing_product") delete data.packages[0].product_id;
+ if (broken === "extra_package")
+ data.packages.push({ ...pack, package_id: "unrequested-package" });
+ buys.set(media_buy_id, {
+ media_buy_id,
+ status: "active",
+ currency: price.currency,
+ total_budget: pkg.budget,
+ confirmed_at: data.confirmed_at,
+ revision: 1,
+ packages: [structuredClone(pack)],
+ });
+ if (mutateAfterCreate) mutateAfterCreate(selected);
+ }
+ } else if (task === "get_media_buys") {
+ assert.equal(reader, true, "an absent reader must never be dispatched");
+ assert.deepEqual(request.account, doc.context.sandbox_account);
+ data = {
+ status: "completed",
+ media_buys: request.media_buy_ids.map((id) =>
+ structuredClone(buys.get(id)),
+ ),
+ };
+ assert.ok(
+ data.media_buys.every(Boolean),
+ "readback uses actual earlier committed buys",
+ );
+ if (broken === "read_identity")
+ data.media_buys[0].media_buy_id = "foreign-buy";
+ if (broken === "read_product")
+ data.media_buys[0].packages[0].product_id = "foreign-product";
+ if (broken === "read_missing_product")
+ delete data.media_buys[0].packages[0].product_id;
+ if (broken === "read_package")
+ data.media_buys[0].packages[0].package_id = "foreign-package";
+ if (broken === "read_widen")
+ data.media_buys[0].packages[0].targeting_overlay.collection_selection.collections =
+ [
+ {
+ publisher_domain: "wrong.example",
+ collection_ids: ["uncommitted"],
+ },
+ ];
+ if (broken === "read_missing")
+ delete data.media_buys[0].packages[0].targeting_overlay;
+ } else assert.fail(`Unexpected tool ${task}`);
+ return reply({
+ content: [{ type: "text", text: JSON.stringify(data) }],
+ ...(task === "get_products" &&
+ responsePadding && { _meta: { padding: "x".repeat(responsePadding) } }),
+ });
+ };
+ let options = {
+ adcpVersion: "3.2.5",
+ timeout_ms: 3000,
+ ...(noProfile
+ ? {}
+ : {
+ profile: {
+ tools,
+ raw_capabilities: caps,
+ supported_protocols: ["media_buy"],
+ },
+ }),
+ transport: { trustedFetchFn },
+ ...(context && { context }),
+ ...overrides,
+ };
+ if (scenarioOnly) return { options, calls, wire, http, doc, url: URL };
+ if (hosted) options = withHostedReportingBudget(doc, options);
+ if (prepareOnly)
+ return {
+ prepared: await prepareStoryboardFixtures(URL, doc, {
+ ...options,
+ brand: doc.context.sandbox_brand,
+ sandbox: true,
+ }),
+ calls,
+ wire,
+ http,
+ };
+ let result;
+ let created;
+ if (
+ !skipPriorCreate &&
+ standalone &&
+ stepId === "read_selection" &&
+ reader &&
+ buyingModes.includes("wholesale")
+ ) {
+ created = await runStoryboardStep(URL, doc, "create_selection", options);
+ if (!continueFailedCreate)
+ assert.equal(created.passed, true, JSON.stringify(created));
+ options = { ...options, context: structuredClone(created.context) };
+ if (priorContextTransform) priorContextTransform(options.context);
+ if (priorRunTransform) priorRunTransform({ doc, options });
+ if (readProfileMode === "probe_error")
+ options.profile = {
+ ...options.profile,
+ capabilities_probe_error: "Controlled prior discovery error",
+ };
+ if (readProfileMode === "schema_error")
+ options.profile = {
+ ...options.profile,
+ capabilities_schema_issues: ["Controlled schema issue"],
+ };
+ if (readProfileMode === "malformed_modes")
+ options.profile = {
+ ...options.profile,
+ raw_capabilities: { ...caps, media_buy: { buying_modes: "wholesale" } },
+ };
+ if (readProfileMode === "fail_once") {
+ delete options.profile;
+ failReadCapabilities = true;
+ }
+ }
+ try {
+ result = standalone
+ ? await runStoryboardStep(readAgentUrl, doc, stepId, options)
+ : await runStoryboard(URL, doc, options);
+ } catch (error) {
+ if (
+ !expectLocalError &&
+ overrides.prepared_fixtures_authorizer === undefined &&
+ overrides.prepared_fixtures === undefined &&
+ !overrides.signal?.aborted
+ )
+ throw error;
+ return { error, calls, wire, http, created };
+ }
+ const graded = (
+ result.phases ?? [{ phase_id: result.phase_id, steps: [result] }]
+ ).flatMap((p) =>
+ p.steps.map((s) => ({
+ ...s,
+ disposition: classifyComplianceStep(s, `${doc.id}/${p.phase_id}`),
+ })),
+ );
+ return { result, calls, wire, http, graded, created };
+}
+
+if (require.main === module) {
+ for (const name of ALL) {
+ test(`${name}: actual source full and standalone accepts native same-graph selectors and commercial terms`, async () => {
+ for (const standalone of [false, true]) {
+ const { result, calls } = await execute(name, { standalone });
+ assert.equal(
+ standalone ? result.passed : result.overall_passed,
+ true,
+ JSON.stringify(result),
+ );
+ assert.equal(
+ result.fixture_resolutions.every(
+ (r) => r.status === "resolved" && r.strategy === "discover",
+ ),
+ true,
+ );
+ assert.ok(calls.some((c) => c.task === "create_media_buy"));
+ }
+ });
+ test(`${name}: nonsupporting catalog is an honest no-match, with no product seeds or purchase`, async () => {
+ const plain = product();
+ delete plain.collections;
+ delete plain.collection_targeting_allowed;
+ for (const standalone of [false, true]) {
+ const { result, calls, graded } = await execute(name, {
+ standalone,
+ catalog: [plain],
+ });
+ assert.equal(
+ calls.some((c) => c.task === "create_media_buy"),
+ false,
+ );
+ assert.equal(
+ standalone
+ ? classifyComplianceStep(
+ result,
+ `${load(name).id}/${result.phase_id}`,
+ )
+ : classifyComplianceStep({
+ skipped: true,
+ skip_reason: result.coverage_gaps?.[0]?.reason,
+ }),
+ "not_applicable",
+ JSON.stringify(result),
+ );
+ assert.equal(
+ graded.some((s) => s.disposition === "failed"),
+ false,
+ );
+ }
+ });
+ }
+ for (const name of READ) {
+ test(`${name}: missing reader has the actual native root gate in full and standalone`, async () => {
+ for (const standalone of [false, true]) {
+ const { result, calls, http, graded } = await execute(name, {
+ reader: false,
+ standalone,
+ stepId: "read_selection",
+ });
+ assert.equal(calls.length, 0, JSON.stringify(calls));
+ assert.equal(http.length, 0);
+ assert.equal(graded.length, 1);
+ assert.equal(
+ graded[0].disposition,
+ "not_applicable",
+ JSON.stringify(result),
+ );
+ assert.equal(graded[0].step_id, "missing_tool");
+ assert.equal(graded[0].phase_id, "missing_tool");
+ }
+ });
+ test(`${name}: actual persisted read passes full and standalone with the committed selector capture`, async () => {
+ for (const standalone of [false, true]) {
+ const { result } = await execute(name, {
+ standalone,
+ stepId: "read_selection",
+ });
+ assert.equal(
+ standalone ? result.passed : result.overall_passed,
+ true,
+ JSON.stringify(result),
+ );
+ }
+ });
+ }
+
+ test("authority: supplied hooks stay fail-closed before every HTTP interaction, not falsely called", async () => {
+ const util = require("node:util");
+ for (const name of ALL)
+ for (const standalone of [false, true]) {
+ let invoked = 0;
+ const audits = [];
+ const { error, http, calls } = await execute(name, {
+ standalone,
+ overrides: {
+ prepared_fixtures_authorizer: () => {
+ invoked++;
+ throw new Error("private-host-token");
+ },
+ prepared_fixtures_audit_sink: (r) => audits.push(r),
+ },
+ });
+ assert.equal(error.name, "PreparedFixturesDeniedError");
+ assert.equal(invoked, 0);
+ assert.equal(http.length, 0);
+ assert.equal(calls.length, 0);
+ assert.equal(audits.length, 1);
+ assert.equal(util.inspect(error).includes("private-host-token"), false);
+ }
+ });
+ test("committed readback: later catalog mutation cannot replace the captured committed set", async () => {
+ for (const standalone of [false, true]) {
+ const { result } = await execute("collection_selection_readback", {
+ standalone,
+ stepId: "read_selection",
+ mutateAfterCreate: (p) => {
+ p.collections = [
+ {
+ publisher_domain: "studio.example",
+ collection_ids: ["later-a", "later-b"],
+ },
+ ];
+ },
+ });
+ assert.equal(
+ standalone ? result.passed : result.overall_passed,
+ true,
+ JSON.stringify(result),
+ );
+ }
+ });
+}
+module.exports = { execute, product, qualifying, CORE, READ, ALL, load, pairs };