@adcp/sdk 17.0.0-beta.6 → 17.0.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/lib/testing/storyboard/default-invariants.js +4 -0
  2. package/dist/lib/testing/storyboard/default-invariants.mjs +4 -0
  3. package/dist/lib/testing/storyboard/loader.js +31 -0
  4. package/dist/lib/testing/storyboard/loader.mjs +31 -0
  5. package/dist/lib/testing/storyboard/pricing-currency-filter.d.mts +13 -0
  6. package/dist/lib/testing/storyboard/pricing-currency-filter.d.ts +13 -0
  7. package/dist/lib/testing/storyboard/pricing-currency-filter.js +175 -0
  8. package/dist/lib/testing/storyboard/pricing-currency-filter.mjs +151 -0
  9. package/dist/lib/testing/storyboard/request-builder.js +6 -3
  10. package/dist/lib/testing/storyboard/request-builder.mjs +6 -3
  11. package/dist/lib/testing/storyboard/required-metric-filters.d.mts +9 -0
  12. package/dist/lib/testing/storyboard/required-metric-filters.d.ts +9 -0
  13. package/dist/lib/testing/storyboard/required-metric-filters.js +128 -0
  14. package/dist/lib/testing/storyboard/required-metric-filters.mjs +104 -0
  15. package/dist/lib/testing/storyboard/runner.js +244 -48
  16. package/dist/lib/testing/storyboard/runner.mjs +246 -49
  17. package/dist/lib/testing/storyboard/seeding.d.mts +86 -1
  18. package/dist/lib/testing/storyboard/seeding.d.ts +86 -1
  19. package/dist/lib/testing/storyboard/seeding.js +91 -18
  20. package/dist/lib/testing/storyboard/seeding.mjs +90 -18
  21. package/dist/lib/testing/storyboard/types.d.mts +30 -1
  22. package/dist/lib/testing/storyboard/types.d.ts +30 -1
  23. package/dist/lib/testing/storyboard/types.js +1 -0
  24. package/dist/lib/testing/storyboard/types.mjs +1 -0
  25. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  26. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  27. package/dist/lib/version.d.mts +3 -3
  28. package/dist/lib/version.d.ts +3 -3
  29. package/dist/lib/version.js +3 -3
  30. package/dist/lib/version.mjs +3 -3
  31. package/docs/TYPE-SUMMARY.md +2 -2
  32. package/docs/guides/WHOLESALE-PAGE-COLLECTION.md +67 -0
  33. package/docs/llms.txt +2 -2
  34. package/docs/migration-14.x-rc-worksheet.md +4 -4
  35. package/package.json +1 -1
@@ -28,6 +28,8 @@ var import_media_buy_status = require('../../utils/media-buy-status.js');
28
28
  var import_state_machine = require('../../server/state-machine.js');
29
29
  var import_assertions = require('./assertions.js');
30
30
  var import_account_sync_contract = require('./account-sync-contract.js');
31
+ var import_pricing_currency_filter = require('./pricing-currency-filter.js');
32
+ var import_required_metric_filters = require('./required-metric-filters.js');
31
33
  const REGISTERED = /* @__PURE__ */ new Set();
32
34
  function registerOnce(id, spec) {
33
35
  if (REGISTERED.has(id)) return;
@@ -36,6 +38,8 @@ function registerOnce(id, spec) {
36
38
  }
37
39
  function registerDefaultInvariants() {
38
40
  registerOnce(import_account_sync_contract.accountSyncContractAssertion.id, import_account_sync_contract.accountSyncContractAssertion);
41
+ registerOnce(import_pricing_currency_filter.pricingCurrencyFilterAssertion.id, import_pricing_currency_filter.pricingCurrencyFilterAssertion);
42
+ registerOnce(import_required_metric_filters.requiredMetricFiltersAssertion.id, import_required_metric_filters.requiredMetricFiltersAssertion);
39
43
  registerOnce("idempotency.conflict_no_payload_leak", {
40
44
  id: "idempotency.conflict_no_payload_leak",
41
45
  default: true,
@@ -8,6 +8,8 @@ import {
8
8
  } from "../../server/state-machine.mjs";
9
9
  import { registerAssertion } from "./assertions.mjs";
10
10
  import { accountSyncContractAssertion } from "./account-sync-contract.mjs";
11
+ import { pricingCurrencyFilterAssertion } from "./pricing-currency-filter.mjs";
12
+ import { requiredMetricFiltersAssertion } from "./required-metric-filters.mjs";
11
13
  const REGISTERED = /* @__PURE__ */ new Set();
12
14
  function registerOnce(id, spec) {
13
15
  if (REGISTERED.has(id)) return;
@@ -16,6 +18,8 @@ function registerOnce(id, spec) {
16
18
  }
17
19
  function registerDefaultInvariants() {
18
20
  registerOnce(accountSyncContractAssertion.id, accountSyncContractAssertion);
21
+ registerOnce(pricingCurrencyFilterAssertion.id, pricingCurrencyFilterAssertion);
22
+ registerOnce(requiredMetricFiltersAssertion.id, requiredMetricFiltersAssertion);
19
23
  registerOnce("idempotency.conflict_no_payload_leak", {
20
24
  id: "idempotency.conflict_no_payload_leak",
21
25
  default: true,
@@ -96,6 +96,37 @@ function validateStoryboardShape(storyboard) {
96
96
  validateBranchSet(storyboard.id, phase);
97
97
  if (!phase.steps) continue;
98
98
  for (const step of phase.steps) {
99
+ if (step.collect_product_pages !== void 0) {
100
+ if (step.collect_product_pages !== true || step.task !== "get_products" || step.sample_request?.buying_mode !== "wholesale") {
101
+ throw new Error(
102
+ `[${storyboard.id}] ${step.id}: collect_product_pages requires get_products with buying_mode: wholesale`
103
+ );
104
+ }
105
+ if (!storyboard.requires?.includes("collect_product_pages_runner")) {
106
+ throw new Error(
107
+ `[${storyboard.id}] ${step.id}: collect_product_pages requires the collect_product_pages_runner runtime requirement`
108
+ );
109
+ }
110
+ const functionalAuth = typeof step.auth === "object" && (step.auth.type === "web_bot_auth" || step.auth.type === "api_key" && step.auth.from_test_kit === true && step.auth.value_strategy === void 0);
111
+ if (step.expect_error || step.parallel_dispatch || step.auth && !functionalAuth || step.not_applicable_if) {
112
+ throw new Error(
113
+ `[${storyboard.id}] ${step.id}: collect_product_pages cannot be combined with error, parallel, auth-probe, or response-skip steps`
114
+ );
115
+ }
116
+ if (Object.hasOwn(step.sample_request, "idempotency_key")) {
117
+ throw new Error(
118
+ `[${storyboard.id}] ${step.id}: collect_product_pages requires fresh reads without idempotency_key`
119
+ );
120
+ }
121
+ const pagination = step.sample_request.pagination;
122
+ if (pagination && typeof pagination === "object" && "cursor" in pagination || ["if_none_match", "if_wholesale_feed_version", "if_pricing_version"].some(
123
+ (field) => step.sample_request?.[field] !== void 0
124
+ )) {
125
+ throw new Error(
126
+ `[${storyboard.id}] ${step.id}: collect_product_pages starts with an unconditional first page, without a cursor`
127
+ );
128
+ }
129
+ }
99
130
  if (step.enrich_request !== void 0 && (step.enrich_request !== true || !step.expect_error || step.negative_path !== "payload_well_formed")) {
100
131
  throw new Error(
101
132
  `[${storyboard.id}] ${step.id}: enrich_request requires expect_error: true and negative_path: payload_well_formed`
@@ -68,6 +68,37 @@ function validateStoryboardShape(storyboard) {
68
68
  validateBranchSet(storyboard.id, phase);
69
69
  if (!phase.steps) continue;
70
70
  for (const step of phase.steps) {
71
+ if (step.collect_product_pages !== void 0) {
72
+ if (step.collect_product_pages !== true || step.task !== "get_products" || step.sample_request?.buying_mode !== "wholesale") {
73
+ throw new Error(
74
+ `[${storyboard.id}] ${step.id}: collect_product_pages requires get_products with buying_mode: wholesale`
75
+ );
76
+ }
77
+ if (!storyboard.requires?.includes("collect_product_pages_runner")) {
78
+ throw new Error(
79
+ `[${storyboard.id}] ${step.id}: collect_product_pages requires the collect_product_pages_runner runtime requirement`
80
+ );
81
+ }
82
+ const functionalAuth = typeof step.auth === "object" && (step.auth.type === "web_bot_auth" || step.auth.type === "api_key" && step.auth.from_test_kit === true && step.auth.value_strategy === void 0);
83
+ if (step.expect_error || step.parallel_dispatch || step.auth && !functionalAuth || step.not_applicable_if) {
84
+ throw new Error(
85
+ `[${storyboard.id}] ${step.id}: collect_product_pages cannot be combined with error, parallel, auth-probe, or response-skip steps`
86
+ );
87
+ }
88
+ if (Object.hasOwn(step.sample_request, "idempotency_key")) {
89
+ throw new Error(
90
+ `[${storyboard.id}] ${step.id}: collect_product_pages requires fresh reads without idempotency_key`
91
+ );
92
+ }
93
+ const pagination = step.sample_request.pagination;
94
+ if (pagination && typeof pagination === "object" && "cursor" in pagination || ["if_none_match", "if_wholesale_feed_version", "if_pricing_version"].some(
95
+ (field) => step.sample_request?.[field] !== void 0
96
+ )) {
97
+ throw new Error(
98
+ `[${storyboard.id}] ${step.id}: collect_product_pages starts with an unconditional first page, without a cursor`
99
+ );
100
+ }
101
+ }
71
102
  if (step.enrich_request !== void 0 && (step.enrich_request !== true || !step.expect_error || step.negative_path !== "payload_well_formed")) {
72
103
  throw new Error(
73
104
  `[${storyboard.id}] ${step.id}: enrich_request requires expect_error: true and negative_path: payload_well_formed`
@@ -0,0 +1,13 @@
1
+ import type { AssertionSpec } from './assertions.mjs';
2
+ /**
3
+ * Validate the observable currency contract on the effective discovery result.
4
+ * This does not compare against fixture rate-card IDs or source currencies:
5
+ * account-specific FX may legally change both media and compulsory signal prices.
6
+ * Normative basis: ProductFilters.pricing_currencies requires every returned
7
+ * product-level pricing option to use a requested currency. Product eligibility
8
+ * (one matching alternative) and pruning returned media options are distinct;
9
+ * optional signal add-ons retain their separate alternative-pricing semantics.
10
+ * https://adcontextprotocol.org/schemas/3.1.29/core/product-filters.json#/properties/pricing_currencies
11
+ * https://docs.adcontextprotocol.org/docs/media-buy/task-reference/get_products#currency-filtering
12
+ */
13
+ export declare const pricingCurrencyFilterAssertion: AssertionSpec;
@@ -0,0 +1,13 @@
1
+ import type { AssertionSpec } from './assertions';
2
+ /**
3
+ * Validate the observable currency contract on the effective discovery result.
4
+ * This does not compare against fixture rate-card IDs or source currencies:
5
+ * account-specific FX may legally change both media and compulsory signal prices.
6
+ * Normative basis: ProductFilters.pricing_currencies requires every returned
7
+ * product-level pricing option to use a requested currency. Product eligibility
8
+ * (one matching alternative) and pruning returned media options are distinct;
9
+ * optional signal add-ons retain their separate alternative-pricing semantics.
10
+ * https://adcontextprotocol.org/schemas/3.1.29/core/product-filters.json#/properties/pricing_currencies
11
+ * https://docs.adcontextprotocol.org/docs/media-buy/task-reference/get_products#currency-filtering
12
+ */
13
+ export declare const pricingCurrencyFilterAssertion: AssertionSpec;
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var pricing_currency_filter_exports = {};
20
+ __export(pricing_currency_filter_exports, {
21
+ pricingCurrencyFilterAssertion: () => pricingCurrencyFilterAssertion
22
+ });
23
+ module.exports = __toCommonJS(pricing_currency_filter_exports);
24
+ const AMOUNT_KEYS = {
25
+ cpm: "cpm",
26
+ percent_of_media: "percent",
27
+ flat_fee: "amount",
28
+ per_unit: "unit_price"
29
+ };
30
+ function record(value) {
31
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
32
+ }
33
+ function records(value) {
34
+ return Array.isArray(value) ? value.filter((entry) => record(entry) !== void 0) : [];
35
+ }
36
+ function signalCompatibility(signal, currencies) {
37
+ if (signal.pricing_options === void 0) return "compatible";
38
+ const prices = records(signal.pricing_options);
39
+ if (prices.length === 0) return "indeterminate";
40
+ let indeterminate = false;
41
+ for (const price of prices) {
42
+ const amountKey = AMOUNT_KEYS[String(price.model)];
43
+ const amount = amountKey ? price[amountKey] : void 0;
44
+ if (amount === 0 || price.model === "percent_of_media" && price.max_cpm === 0) return "compatible";
45
+ if (typeof price.currency === "string" && currencies.has(price.currency)) return "compatible";
46
+ if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0 || typeof price.currency !== "string") {
47
+ indeterminate = true;
48
+ }
49
+ }
50
+ return indeterminate ? "indeterminate" : "incompatible";
51
+ }
52
+ function groupRulesFor(signal, rules) {
53
+ const modes = Array.isArray(signal.allowed_targeting_modes) ? signal.allowed_targeting_modes : ["include"];
54
+ return records(rules.selection_group_rules).filter(
55
+ (group) => group.selection_group === signal.selection_group && (group.targeting_mode === void 0 || modes.includes(group.targeting_mode))
56
+ );
57
+ }
58
+ function isFixedSelection(signal, rules) {
59
+ if (signal.default_selected !== true) return false;
60
+ const overrides = groupRulesFor(signal, rules);
61
+ return overrides.length > 0 ? overrides.some((group) => (group.selection_mode ?? rules.selection_mode) === "fixed") : rules.selection_mode === "fixed";
62
+ }
63
+ function selectionCapacity(options, compatible, rules, possible) {
64
+ const grouped = /* @__PURE__ */ new Map();
65
+ for (const [index, option] of options.entries()) {
66
+ if (compatible[index] !== "compatible" && !(possible && compatible[index] === "indeterminate")) continue;
67
+ const group = option.selection_group ?? option;
68
+ grouped.set(group, (grouped.get(group) ?? 0) + 1);
69
+ }
70
+ const perGroup = typeof rules.max_selected_per_group === "number" ? rules.max_selected_per_group : Infinity;
71
+ const capacity = [...grouped.values()].reduce((sum, count) => sum + Math.min(count, perGroup), 0);
72
+ return typeof rules.max_selected_signals === "number" ? Math.min(capacity, rules.max_selected_signals) : capacity;
73
+ }
74
+ const pricingCurrencyFilterAssertion = {
75
+ id: "discovery.pricing_currency_filter",
76
+ default: true,
77
+ description: "Returned media prices and compulsory product-scoped signal charges satisfy filters.pricing_currencies.",
78
+ onStep(_ctx, step) {
79
+ if (step.skipped || step.task !== "get_products") return [];
80
+ const request = record(step.request?.payload);
81
+ const requested = record(request?.filters)?.pricing_currencies;
82
+ if (!Array.isArray(requested) || requested.length === 0 || !requested.every((currency) => typeof currency === "string")) {
83
+ return [];
84
+ }
85
+ const raw = record(step.response ?? step.response_record?.payload);
86
+ const response = Array.isArray(raw?.products) ? raw : record(raw?.data) ?? raw;
87
+ if (!Array.isArray(response?.products)) return [];
88
+ if (response.products.length === 0) {
89
+ return [
90
+ {
91
+ passed: true,
92
+ status: "not_applicable",
93
+ step_id: step.step_id,
94
+ description: "No currency-compatible products were returned; positive media-pricing coverage is unproven"
95
+ }
96
+ ];
97
+ }
98
+ const currencies = new Set(requested);
99
+ const findings = [];
100
+ const add = (path, compatibility) => {
101
+ findings.push({
102
+ passed: compatibility !== "incompatible",
103
+ step_id: step.step_id,
104
+ description: compatibility === "indeterminate" ? `${path}: pricing currency compatibility is unproven for custom or incomplete signal pricing` : `${path}: pricing must be satisfiable in a requested currency`,
105
+ ...compatibility === "indeterminate" && { status: "not_applicable" },
106
+ ...compatibility === "incompatible" && {
107
+ error: `${path} cannot be satisfied in filters.pricing_currencies ${JSON.stringify(requested)}`
108
+ }
109
+ });
110
+ };
111
+ for (const [productIndex, product] of records(response.products).entries()) {
112
+ const productPath = `products[${productIndex}]`;
113
+ for (const [priceIndex, price] of records(product.pricing_options).entries()) {
114
+ add(
115
+ `${productPath}.pricing_options[${priceIndex}].currency`,
116
+ typeof price.currency === "string" && currencies.has(price.currency) ? "compatible" : "incompatible"
117
+ );
118
+ }
119
+ const rules = record(product.signal_targeting_rules) ?? {};
120
+ const signals = records(product.signal_targeting_options);
121
+ const groups = records(rules.selection_group_rules);
122
+ if (product.signal_targeting_options === void 0 && (rules.selection_mode === "fixed" || rules.selection_mode === "required" || groups.some((group) => ["fixed", "required"].includes(String(group.selection_mode))))) {
123
+ add(`${productPath}.signal_targeting_options: mandatory inline menu is unavailable`, "indeterminate");
124
+ }
125
+ for (const [signalIndex, signal] of signals.entries()) {
126
+ if (isFixedSelection(signal, rules)) {
127
+ add(`${productPath}.signal_targeting_options[${signalIndex}]`, signalCompatibility(signal, currencies));
128
+ }
129
+ }
130
+ const requiredGroups = records(rules.selection_group_rules).map((group, index) => ({ group, index })).filter(({ group }) => (group.selection_mode ?? rules.selection_mode) === "required");
131
+ const requirements = requiredGroups.map(({ group, index }) => ({
132
+ path: `${productPath}.signal_targeting_rules.selection_group_rules[${index}]`,
133
+ minimum: group.min_selected_signals ?? 1,
134
+ options: signals.filter((signal) => groupRulesFor(signal, rules).includes(group)),
135
+ bounds: {
136
+ ...rules,
137
+ max_selected_signals: Math.min(
138
+ typeof rules.max_selected_signals === "number" ? rules.max_selected_signals : Infinity,
139
+ typeof group.max_selected_signals === "number" ? group.max_selected_signals : Infinity
140
+ )
141
+ }
142
+ }));
143
+ if (rules.selection_mode === "required" && records(rules.selection_group_rules).length === 0) {
144
+ requirements.push({
145
+ path: `${productPath}.signal_targeting_rules`,
146
+ minimum: rules.min_selected_signals ?? 1,
147
+ options: signals,
148
+ bounds: rules
149
+ });
150
+ } else if (rules.selection_mode === "required") {
151
+ findings.push({
152
+ passed: true,
153
+ status: "not_applicable",
154
+ step_id: step.step_id,
155
+ description: `${productPath}.signal_targeting_rules: aggregate required-selection feasibility with group overrides is unproven`
156
+ });
157
+ }
158
+ for (const requirement of requirements) {
159
+ if (product.signal_targeting_options === void 0 || typeof requirement.minimum !== "number") continue;
160
+ const compatible = requirement.options.map((signal) => signalCompatibility(signal, currencies));
161
+ const proven = selectionCapacity(requirement.options, compatible, requirement.bounds, false);
162
+ const possible = selectionCapacity(requirement.options, compatible, requirement.bounds, true);
163
+ add(
164
+ requirement.path,
165
+ proven >= requirement.minimum ? "compatible" : possible >= requirement.minimum ? "indeterminate" : "incompatible"
166
+ );
167
+ }
168
+ }
169
+ return findings;
170
+ }
171
+ };
172
+ // Annotate the CommonJS export names for ESM import in node:
173
+ 0 && (module.exports = {
174
+ pricingCurrencyFilterAssertion
175
+ });
@@ -0,0 +1,151 @@
1
+ const AMOUNT_KEYS = {
2
+ cpm: "cpm",
3
+ percent_of_media: "percent",
4
+ flat_fee: "amount",
5
+ per_unit: "unit_price"
6
+ };
7
+ function record(value) {
8
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
9
+ }
10
+ function records(value) {
11
+ return Array.isArray(value) ? value.filter((entry) => record(entry) !== void 0) : [];
12
+ }
13
+ function signalCompatibility(signal, currencies) {
14
+ if (signal.pricing_options === void 0) return "compatible";
15
+ const prices = records(signal.pricing_options);
16
+ if (prices.length === 0) return "indeterminate";
17
+ let indeterminate = false;
18
+ for (const price of prices) {
19
+ const amountKey = AMOUNT_KEYS[String(price.model)];
20
+ const amount = amountKey ? price[amountKey] : void 0;
21
+ if (amount === 0 || price.model === "percent_of_media" && price.max_cpm === 0) return "compatible";
22
+ if (typeof price.currency === "string" && currencies.has(price.currency)) return "compatible";
23
+ if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0 || typeof price.currency !== "string") {
24
+ indeterminate = true;
25
+ }
26
+ }
27
+ return indeterminate ? "indeterminate" : "incompatible";
28
+ }
29
+ function groupRulesFor(signal, rules) {
30
+ const modes = Array.isArray(signal.allowed_targeting_modes) ? signal.allowed_targeting_modes : ["include"];
31
+ return records(rules.selection_group_rules).filter(
32
+ (group) => group.selection_group === signal.selection_group && (group.targeting_mode === void 0 || modes.includes(group.targeting_mode))
33
+ );
34
+ }
35
+ function isFixedSelection(signal, rules) {
36
+ if (signal.default_selected !== true) return false;
37
+ const overrides = groupRulesFor(signal, rules);
38
+ return overrides.length > 0 ? overrides.some((group) => (group.selection_mode ?? rules.selection_mode) === "fixed") : rules.selection_mode === "fixed";
39
+ }
40
+ function selectionCapacity(options, compatible, rules, possible) {
41
+ const grouped = /* @__PURE__ */ new Map();
42
+ for (const [index, option] of options.entries()) {
43
+ if (compatible[index] !== "compatible" && !(possible && compatible[index] === "indeterminate")) continue;
44
+ const group = option.selection_group ?? option;
45
+ grouped.set(group, (grouped.get(group) ?? 0) + 1);
46
+ }
47
+ const perGroup = typeof rules.max_selected_per_group === "number" ? rules.max_selected_per_group : Infinity;
48
+ const capacity = [...grouped.values()].reduce((sum, count) => sum + Math.min(count, perGroup), 0);
49
+ return typeof rules.max_selected_signals === "number" ? Math.min(capacity, rules.max_selected_signals) : capacity;
50
+ }
51
+ const pricingCurrencyFilterAssertion = {
52
+ id: "discovery.pricing_currency_filter",
53
+ default: true,
54
+ description: "Returned media prices and compulsory product-scoped signal charges satisfy filters.pricing_currencies.",
55
+ onStep(_ctx, step) {
56
+ if (step.skipped || step.task !== "get_products") return [];
57
+ const request = record(step.request?.payload);
58
+ const requested = record(request?.filters)?.pricing_currencies;
59
+ if (!Array.isArray(requested) || requested.length === 0 || !requested.every((currency) => typeof currency === "string")) {
60
+ return [];
61
+ }
62
+ const raw = record(step.response ?? step.response_record?.payload);
63
+ const response = Array.isArray(raw?.products) ? raw : record(raw?.data) ?? raw;
64
+ if (!Array.isArray(response?.products)) return [];
65
+ if (response.products.length === 0) {
66
+ return [
67
+ {
68
+ passed: true,
69
+ status: "not_applicable",
70
+ step_id: step.step_id,
71
+ description: "No currency-compatible products were returned; positive media-pricing coverage is unproven"
72
+ }
73
+ ];
74
+ }
75
+ const currencies = new Set(requested);
76
+ const findings = [];
77
+ const add = (path, compatibility) => {
78
+ findings.push({
79
+ passed: compatibility !== "incompatible",
80
+ step_id: step.step_id,
81
+ description: compatibility === "indeterminate" ? `${path}: pricing currency compatibility is unproven for custom or incomplete signal pricing` : `${path}: pricing must be satisfiable in a requested currency`,
82
+ ...compatibility === "indeterminate" && { status: "not_applicable" },
83
+ ...compatibility === "incompatible" && {
84
+ error: `${path} cannot be satisfied in filters.pricing_currencies ${JSON.stringify(requested)}`
85
+ }
86
+ });
87
+ };
88
+ for (const [productIndex, product] of records(response.products).entries()) {
89
+ const productPath = `products[${productIndex}]`;
90
+ for (const [priceIndex, price] of records(product.pricing_options).entries()) {
91
+ add(
92
+ `${productPath}.pricing_options[${priceIndex}].currency`,
93
+ typeof price.currency === "string" && currencies.has(price.currency) ? "compatible" : "incompatible"
94
+ );
95
+ }
96
+ const rules = record(product.signal_targeting_rules) ?? {};
97
+ const signals = records(product.signal_targeting_options);
98
+ const groups = records(rules.selection_group_rules);
99
+ if (product.signal_targeting_options === void 0 && (rules.selection_mode === "fixed" || rules.selection_mode === "required" || groups.some((group) => ["fixed", "required"].includes(String(group.selection_mode))))) {
100
+ add(`${productPath}.signal_targeting_options: mandatory inline menu is unavailable`, "indeterminate");
101
+ }
102
+ for (const [signalIndex, signal] of signals.entries()) {
103
+ if (isFixedSelection(signal, rules)) {
104
+ add(`${productPath}.signal_targeting_options[${signalIndex}]`, signalCompatibility(signal, currencies));
105
+ }
106
+ }
107
+ const requiredGroups = records(rules.selection_group_rules).map((group, index) => ({ group, index })).filter(({ group }) => (group.selection_mode ?? rules.selection_mode) === "required");
108
+ const requirements = requiredGroups.map(({ group, index }) => ({
109
+ path: `${productPath}.signal_targeting_rules.selection_group_rules[${index}]`,
110
+ minimum: group.min_selected_signals ?? 1,
111
+ options: signals.filter((signal) => groupRulesFor(signal, rules).includes(group)),
112
+ bounds: {
113
+ ...rules,
114
+ max_selected_signals: Math.min(
115
+ typeof rules.max_selected_signals === "number" ? rules.max_selected_signals : Infinity,
116
+ typeof group.max_selected_signals === "number" ? group.max_selected_signals : Infinity
117
+ )
118
+ }
119
+ }));
120
+ if (rules.selection_mode === "required" && records(rules.selection_group_rules).length === 0) {
121
+ requirements.push({
122
+ path: `${productPath}.signal_targeting_rules`,
123
+ minimum: rules.min_selected_signals ?? 1,
124
+ options: signals,
125
+ bounds: rules
126
+ });
127
+ } else if (rules.selection_mode === "required") {
128
+ findings.push({
129
+ passed: true,
130
+ status: "not_applicable",
131
+ step_id: step.step_id,
132
+ description: `${productPath}.signal_targeting_rules: aggregate required-selection feasibility with group overrides is unproven`
133
+ });
134
+ }
135
+ for (const requirement of requirements) {
136
+ if (product.signal_targeting_options === void 0 || typeof requirement.minimum !== "number") continue;
137
+ const compatible = requirement.options.map((signal) => signalCompatibility(signal, currencies));
138
+ const proven = selectionCapacity(requirement.options, compatible, requirement.bounds, false);
139
+ const possible = selectionCapacity(requirement.options, compatible, requirement.bounds, true);
140
+ add(
141
+ requirement.path,
142
+ proven >= requirement.minimum ? "compatible" : possible >= requirement.minimum ? "indeterminate" : "incompatible"
143
+ );
144
+ }
145
+ }
146
+ return findings;
147
+ }
148
+ };
149
+ export {
150
+ pricingCurrencyFilterAssertion
151
+ };
@@ -30,6 +30,7 @@ var import_client = require('../client.js');
30
30
  var import_context = require('./context.js');
31
31
  var import_pricing_adapter = require('../../utils/pricing-adapter.js');
32
32
  const UNKNOWN_FORMAT_ID = Object.freeze({ agent_url: "https://unknown.example.com/", id: "unknown" });
33
+ const AUCTION_PRICING_MODELS = /* @__PURE__ */ new Set(["cpm", "vcpm", "cpc", "cpcv", "cpv", "cpp", "flat_rate", "time"]);
33
34
  const FALLBACK_CALLER_AGENT_URL = "https://e2e-orchestrator.adcontextprotocol.org/";
34
35
  const FIXTURE_AWARE_ENRICHERS = /* @__PURE__ */ new Set([
35
36
  "create_media_buy",
@@ -252,9 +253,11 @@ const REQUEST_ENRICHERS = {
252
253
  const packages = [firstPkg, ...additionalPkgs];
253
254
  for (const pkg of packages) {
254
255
  const sentPricingOption = pkg === firstPkg && fixtureProductId === void 0 && fixturePricingOptionId === void 0 ? pricingOption : findPricingOption(context, pkg.product_id, pkg.pricing_option_id);
255
- if (pkg.bid_price === void 0 && pkg.bidding === void 0 && step.sample_request?.bidding === void 0 && sentPricingOption?.pricing_model === "cpm" && !(0, import_pricing_adapter.isFixedPricing)(sentPricingOption)) {
256
- const floor = Number(sentPricingOption.floor_price) || 5;
257
- pkg.bid_price = Math.round(floor * 1.5 * 100) / 100;
256
+ if (pkg.bid_price === void 0 && pkg.bidding === void 0 && step.sample_request?.bidding === void 0 && sentPricingOption !== void 0 && AUCTION_PRICING_MODELS.has(sentPricingOption.pricing_model) && !(0, import_pricing_adapter.isFixedPricing)(sentPricingOption)) {
257
+ const floor = sentPricingOption.floor_price === void 0 ? 5 : sentPricingOption.floor_price;
258
+ if (typeof floor !== "number" || !Number.isFinite(floor) || floor < 0) continue;
259
+ const roundedBid = Math.round(floor * 150) / 100;
260
+ pkg.bid_price = Number.isFinite(roundedBid) ? Math.max(floor, roundedBid) : floor;
258
261
  }
259
262
  }
260
263
  const fixture = step.sample_request !== void 0 ? (0, import_context.injectContext)({ ...step.sample_request }, context) : {};
@@ -4,6 +4,7 @@ import { resolveBrand, resolveAccount } from "../client.mjs";
4
4
  import { injectContext } from "./context.mjs";
5
5
  import { isFixedPricing } from "../../utils/pricing-adapter.mjs";
6
6
  const UNKNOWN_FORMAT_ID = Object.freeze({ agent_url: "https://unknown.example.com/", id: "unknown" });
7
+ const AUCTION_PRICING_MODELS = /* @__PURE__ */ new Set(["cpm", "vcpm", "cpc", "cpcv", "cpv", "cpp", "flat_rate", "time"]);
7
8
  const FALLBACK_CALLER_AGENT_URL = "https://e2e-orchestrator.adcontextprotocol.org/";
8
9
  const FIXTURE_AWARE_ENRICHERS = /* @__PURE__ */ new Set([
9
10
  "create_media_buy",
@@ -226,9 +227,11 @@ const REQUEST_ENRICHERS = {
226
227
  const packages = [firstPkg, ...additionalPkgs];
227
228
  for (const pkg of packages) {
228
229
  const sentPricingOption = pkg === firstPkg && fixtureProductId === void 0 && fixturePricingOptionId === void 0 ? pricingOption : findPricingOption(context, pkg.product_id, pkg.pricing_option_id);
229
- if (pkg.bid_price === void 0 && pkg.bidding === void 0 && step.sample_request?.bidding === void 0 && sentPricingOption?.pricing_model === "cpm" && !isFixedPricing(sentPricingOption)) {
230
- const floor = Number(sentPricingOption.floor_price) || 5;
231
- pkg.bid_price = Math.round(floor * 1.5 * 100) / 100;
230
+ if (pkg.bid_price === void 0 && pkg.bidding === void 0 && step.sample_request?.bidding === void 0 && sentPricingOption !== void 0 && AUCTION_PRICING_MODELS.has(sentPricingOption.pricing_model) && !isFixedPricing(sentPricingOption)) {
231
+ const floor = sentPricingOption.floor_price === void 0 ? 5 : sentPricingOption.floor_price;
232
+ if (typeof floor !== "number" || !Number.isFinite(floor) || floor < 0) continue;
233
+ const roundedBid = Math.round(floor * 150) / 100;
234
+ pkg.bid_price = Number.isFinite(roundedBid) ? Math.max(floor, roundedBid) : floor;
232
235
  }
233
236
  }
234
237
  const fixture = step.sample_request !== void 0 ? injectContext({ ...step.sample_request }, context) : {};
@@ -0,0 +1,9 @@
1
+ import type { AssertionSpec } from './assertions.mjs';
2
+ /**
3
+ * Optional discovery filters have binding semantics when a buyer supplies them.
4
+ * Normative basis: ProductFilters.required_metrics and required_vendor_metrics
5
+ * require every returned product to satisfy the supplied metric predicates.
6
+ * https://adcontextprotocol.org/schemas/3.1.29/core/product-filters.json#/properties/required_metrics
7
+ * https://adcontextprotocol.org/schemas/3.1.29/core/product-filters.json#/properties/required_vendor_metrics
8
+ */
9
+ export declare const requiredMetricFiltersAssertion: AssertionSpec;
@@ -0,0 +1,9 @@
1
+ import type { AssertionSpec } from './assertions';
2
+ /**
3
+ * Optional discovery filters have binding semantics when a buyer supplies them.
4
+ * Normative basis: ProductFilters.required_metrics and required_vendor_metrics
5
+ * require every returned product to satisfy the supplied metric predicates.
6
+ * https://adcontextprotocol.org/schemas/3.1.29/core/product-filters.json#/properties/required_metrics
7
+ * https://adcontextprotocol.org/schemas/3.1.29/core/product-filters.json#/properties/required_vendor_metrics
8
+ */
9
+ export declare const requiredMetricFiltersAssertion: AssertionSpec;