@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.
- package/dist/lib/testing/storyboard/default-invariants.js +4 -0
- package/dist/lib/testing/storyboard/default-invariants.mjs +4 -0
- package/dist/lib/testing/storyboard/loader.js +31 -0
- package/dist/lib/testing/storyboard/loader.mjs +31 -0
- package/dist/lib/testing/storyboard/pricing-currency-filter.d.mts +13 -0
- package/dist/lib/testing/storyboard/pricing-currency-filter.d.ts +13 -0
- package/dist/lib/testing/storyboard/pricing-currency-filter.js +175 -0
- package/dist/lib/testing/storyboard/pricing-currency-filter.mjs +151 -0
- package/dist/lib/testing/storyboard/request-builder.js +6 -3
- package/dist/lib/testing/storyboard/request-builder.mjs +6 -3
- package/dist/lib/testing/storyboard/required-metric-filters.d.mts +9 -0
- package/dist/lib/testing/storyboard/required-metric-filters.d.ts +9 -0
- package/dist/lib/testing/storyboard/required-metric-filters.js +128 -0
- package/dist/lib/testing/storyboard/required-metric-filters.mjs +104 -0
- package/dist/lib/testing/storyboard/runner.js +244 -48
- package/dist/lib/testing/storyboard/runner.mjs +246 -49
- package/dist/lib/testing/storyboard/seeding.d.mts +86 -1
- package/dist/lib/testing/storyboard/seeding.d.ts +86 -1
- package/dist/lib/testing/storyboard/seeding.js +91 -18
- package/dist/lib/testing/storyboard/seeding.mjs +90 -18
- package/dist/lib/testing/storyboard/types.d.mts +30 -1
- package/dist/lib/testing/storyboard/types.d.ts +30 -1
- package/dist/lib/testing/storyboard/types.js +1 -0
- package/dist/lib/testing/storyboard/types.mjs +1 -0
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.mjs +3 -3
- package/docs/TYPE-SUMMARY.md +2 -2
- package/docs/guides/WHOLESALE-PAGE-COLLECTION.md +67 -0
- package/docs/llms.txt +2 -2
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- 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
|
|
256
|
-
const floor =
|
|
257
|
-
|
|
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
|
|
230
|
-
const floor =
|
|
231
|
-
|
|
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;
|