@adcp/sdk 17.0.0-beta.5 → 17.0.0-beta.7

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 (49) hide show
  1. package/dist/lib/identity-3.3/validation.js +3 -1
  2. package/dist/lib/identity-3.3/validation.mjs +8 -2
  3. package/dist/lib/schemas/index.d.mts +2 -2
  4. package/dist/lib/schemas/index.d.ts +2 -2
  5. package/dist/lib/server/create-adcp-server.js +8 -2
  6. package/dist/lib/server/create-adcp-server.mjs +9 -3
  7. package/dist/lib/testing/storyboard/default-invariants.js +4 -0
  8. package/dist/lib/testing/storyboard/default-invariants.mjs +4 -0
  9. package/dist/lib/testing/storyboard/loader.js +31 -0
  10. package/dist/lib/testing/storyboard/loader.mjs +31 -0
  11. package/dist/lib/testing/storyboard/pricing-currency-filter.d.mts +13 -0
  12. package/dist/lib/testing/storyboard/pricing-currency-filter.d.ts +13 -0
  13. package/dist/lib/testing/storyboard/pricing-currency-filter.js +175 -0
  14. package/dist/lib/testing/storyboard/pricing-currency-filter.mjs +151 -0
  15. package/dist/lib/testing/storyboard/required-metric-filters.d.mts +9 -0
  16. package/dist/lib/testing/storyboard/required-metric-filters.d.ts +9 -0
  17. package/dist/lib/testing/storyboard/required-metric-filters.js +128 -0
  18. package/dist/lib/testing/storyboard/required-metric-filters.mjs +104 -0
  19. package/dist/lib/testing/storyboard/runner.js +244 -48
  20. package/dist/lib/testing/storyboard/runner.mjs +246 -49
  21. package/dist/lib/testing/storyboard/seeding.d.mts +86 -1
  22. package/dist/lib/testing/storyboard/seeding.d.ts +86 -1
  23. package/dist/lib/testing/storyboard/seeding.js +91 -18
  24. package/dist/lib/testing/storyboard/seeding.mjs +90 -18
  25. package/dist/lib/testing/storyboard/types.d.mts +30 -1
  26. package/dist/lib/testing/storyboard/types.d.ts +30 -1
  27. package/dist/lib/testing/storyboard/types.js +1 -0
  28. package/dist/lib/testing/storyboard/types.mjs +1 -0
  29. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  30. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  31. package/dist/lib/utils/envelope-status-compat.d.mts +21 -2
  32. package/dist/lib/utils/envelope-status-compat.d.ts +21 -2
  33. package/dist/lib/utils/envelope-status-compat.js +21 -6
  34. package/dist/lib/utils/envelope-status-compat.mjs +23 -4
  35. package/dist/lib/utils/response-schemas.js +4 -0
  36. package/dist/lib/utils/response-schemas.mjs +5 -1
  37. package/dist/lib/validation/schema-loader.d.mts +2 -2
  38. package/dist/lib/validation/schema-loader.d.ts +2 -2
  39. package/dist/lib/validation/schema-loader.js +72 -9
  40. package/dist/lib/validation/schema-loader.mjs +69 -8
  41. package/dist/lib/version.d.mts +3 -3
  42. package/dist/lib/version.d.ts +3 -3
  43. package/dist/lib/version.js +3 -3
  44. package/dist/lib/version.mjs +3 -3
  45. package/docs/TYPE-SUMMARY.md +2 -2
  46. package/docs/guides/WHOLESALE-PAGE-COLLECTION.md +67 -0
  47. package/docs/llms.txt +2 -2
  48. package/docs/migration-14.x-rc-worksheet.md +4 -4
  49. package/package.json +1 -1
@@ -81,7 +81,9 @@ function validateIdentity33Payload(tool, direction, payload, version) {
81
81
  };
82
82
  var walk = walk2;
83
83
  const document = (0, import_schema_loader.getToolSchemaDocument)(tool, direction === "request" ? "request" : "sync", version)?.schema ?? stable.schema;
84
- const schema = structuredClone(direction === "response" ? (0, import_schema_loader.relaxResponseRoot)(document) : document);
84
+ const schema = structuredClone(
85
+ direction === "response" ? (0, import_schema_loader.acceptEnvelopeOnClosedResponseRootForVersion)(document, version) : document
86
+ );
85
87
  const relationship = import_schemas.default.find((s) => s.$id === "/schemas/core/principal-relationship.json");
86
88
  const onboarding = import_schemas.default.find((s) => s.$id === "/schemas/core/agent-onboarding.json");
87
89
  walk2(schema);
@@ -1,6 +1,10 @@
1
1
  import Ajv from "ajv";
2
2
  import addFormats from "ajv-formats";
3
- import { getValidator, getToolSchemaDocument, relaxResponseRoot } from "../validation/schema-loader.mjs";
3
+ import {
4
+ getValidator,
5
+ getToolSchemaDocument,
6
+ acceptEnvelopeOnClosedResponseRootForVersion
7
+ } from "../validation/schema-loader.mjs";
4
8
  import { compactSchemaForValidation } from "../validation/compact-schema.mjs";
5
9
  import schemas from "./schemas.draft.mjs";
6
10
  const validators = /* @__PURE__ */ new WeakMap();
@@ -48,7 +52,9 @@ function validateIdentity33Payload(tool, direction, payload, version) {
48
52
  };
49
53
  var walk = walk2;
50
54
  const document = getToolSchemaDocument(tool, direction === "request" ? "request" : "sync", version)?.schema ?? stable.schema;
51
- const schema = structuredClone(direction === "response" ? relaxResponseRoot(document) : document);
55
+ const schema = structuredClone(
56
+ direction === "response" ? acceptEnvelopeOnClosedResponseRootForVersion(document, version) : document
57
+ );
52
58
  const relationship = schemas.find((s) => s.$id === "/schemas/core/principal-relationship.json");
53
59
  const onboarding = schemas.find((s) => s.$id === "/schemas/core/agent-onboarding.json");
54
60
  walk2(schema);
@@ -81,8 +81,8 @@ export declare function getToolResponseSchema(toolName: string, options?: ToolRe
81
81
  /**
82
82
  * Compile the exact protocol-authored JSON Schema for offline conformance and
83
83
  * CI validation. Unlike the SDK's live-wire response validation, this does not
84
- * apply `relaxResponseRoot`, so extensible envelope fields can be rejected when
85
- * the selected normative schema does not declare them.
84
+ * widen closed roots for envelope fields, so a release whose normative schema
85
+ * does not declare an envelope field rejects it.
86
86
  *
87
87
  * This validator is not intended for validating live wire responses, whose
88
88
  * transport envelope can carry extension fields. It uses `allErrors: true`, so
@@ -81,8 +81,8 @@ export declare function getToolResponseSchema(toolName: string, options?: ToolRe
81
81
  /**
82
82
  * Compile the exact protocol-authored JSON Schema for offline conformance and
83
83
  * CI validation. Unlike the SDK's live-wire response validation, this does not
84
- * apply `relaxResponseRoot`, so extensible envelope fields can be rejected when
85
- * the selected normative schema does not declare them.
84
+ * widen closed roots for envelope fields, so a release whose normative schema
85
+ * does not declare an envelope field rejects it.
86
86
  *
87
87
  * This validator is not intended for validating live wire responses, whose
88
88
  * transport envelope can carry extension fields. It uses `allErrors: true`, so
@@ -363,7 +363,6 @@ function buildIdempotencyPayload(toolName, params, account, sessionKey) {
363
363
  }
364
364
  function genericResponse(toolName, data, summary) {
365
365
  const structuredContent = { ...(0, import_responses.toStructuredContent)(data) };
366
- if (import_envelope_status_compat.STATUS_FREE_SYNC_RESPONSE_TOOLS.has(toolName)) delete structuredContent.status;
367
366
  return {
368
367
  content: [{ type: "text", text: summary ?? `${toolName} completed` }],
369
368
  structuredContent
@@ -1407,8 +1406,15 @@ function injectEnvelopeStatusIntoResponse(response, toolName, servedVersion) {
1407
1406
  return;
1408
1407
  const sc = response.structuredContent;
1409
1408
  if (!sc || typeof sc !== "object") return;
1409
+ if ((0, import_envelope_status_compat.omitsEnvelopeStatusForRelease)(toolName, servedVersion)) {
1410
+ if ("status" in sc) {
1411
+ const mirrorsStructuredContent2 = contentTextMirrorsStructuredContent(response, sc);
1412
+ delete sc.status;
1413
+ syncContentJsonText(response, sc, mirrorsStructuredContent2);
1414
+ }
1415
+ return;
1416
+ }
1410
1417
  if ("status" in sc) return;
1411
- if (import_envelope_status_compat.STATUS_FREE_SYNC_RESPONSE_TOOLS.has(toolName)) return;
1412
1418
  const status = response.isError === true ? "failed" : "completed";
1413
1419
  const mirrorsStructuredContent = contentTextMirrorsStructuredContent(response, sc);
1414
1420
  sc.status = status;
@@ -133,7 +133,7 @@ const IDEMPOTENCY_CLAIM_RENEW_INTERVAL_MS = 6e4;
133
133
  import { isAccountProvisioningTask } from "./account-provisioning.mjs";
134
134
  import { warnAccountReferenceDeprecation } from "./account-reference-warnings.mjs";
135
135
  import { isMutatingTask, requestUsesIdempotency, IDEMPOTENCY_KEY_PATTERN, MUTATING_TASKS } from "../utils/idempotency.mjs";
136
- import { STATUS_FREE_SYNC_RESPONSE_TOOLS } from "../utils/envelope-status-compat.mjs";
136
+ import { omitsEnvelopeStatusForRelease } from "../utils/envelope-status-compat.mjs";
137
137
  import { validateRequest, validateResponse, formatIssues } from "../validation/schema-validator.mjs";
138
138
  import { validateSyncReportingStatusEnvelope } from "../validation/sync-reporting-status-envelope.mjs";
139
139
  import { buildAdcpValidationErrorPayload } from "../validation/schema-errors.mjs";
@@ -465,7 +465,6 @@ function buildIdempotencyPayload(toolName, params, account, sessionKey) {
465
465
  }
466
466
  function genericResponse(toolName, data, summary) {
467
467
  const structuredContent = { ...toStructuredContent(data) };
468
- if (STATUS_FREE_SYNC_RESPONSE_TOOLS.has(toolName)) delete structuredContent.status;
469
468
  return {
470
469
  content: [{ type: "text", text: summary ?? `${toolName} completed` }],
471
470
  structuredContent
@@ -1509,8 +1508,15 @@ function injectEnvelopeStatusIntoResponse(response, toolName, servedVersion) {
1509
1508
  return;
1510
1509
  const sc = response.structuredContent;
1511
1510
  if (!sc || typeof sc !== "object") return;
1511
+ if (omitsEnvelopeStatusForRelease(toolName, servedVersion)) {
1512
+ if ("status" in sc) {
1513
+ const mirrorsStructuredContent2 = contentTextMirrorsStructuredContent(response, sc);
1514
+ delete sc.status;
1515
+ syncContentJsonText(response, sc, mirrorsStructuredContent2);
1516
+ }
1517
+ return;
1518
+ }
1512
1519
  if ("status" in sc) return;
1513
- if (STATUS_FREE_SYNC_RESPONSE_TOOLS.has(toolName)) return;
1514
1520
  const status = response.isError === true ? "failed" : "completed";
1515
1521
  const mirrorsStructuredContent = contentTextMirrorsStructuredContent(response, sc);
1516
1522
  sc.status = status;
@@ -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
+ };
@@ -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;