@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.
- package/dist/lib/identity-3.3/validation.js +3 -1
- package/dist/lib/identity-3.3/validation.mjs +8 -2
- package/dist/lib/schemas/index.d.mts +2 -2
- package/dist/lib/schemas/index.d.ts +2 -2
- package/dist/lib/server/create-adcp-server.js +8 -2
- package/dist/lib/server/create-adcp-server.mjs +9 -3
- 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/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/utils/envelope-status-compat.d.mts +21 -2
- package/dist/lib/utils/envelope-status-compat.d.ts +21 -2
- package/dist/lib/utils/envelope-status-compat.js +21 -6
- package/dist/lib/utils/envelope-status-compat.mjs +23 -4
- package/dist/lib/utils/response-schemas.js +4 -0
- package/dist/lib/utils/response-schemas.mjs +5 -1
- package/dist/lib/validation/schema-loader.d.mts +2 -2
- package/dist/lib/validation/schema-loader.d.ts +2 -2
- package/dist/lib/validation/schema-loader.js +72 -9
- package/dist/lib/validation/schema-loader.mjs +69 -8
- 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
|
@@ -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(
|
|
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 {
|
|
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(
|
|
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
|
-
*
|
|
85
|
-
*
|
|
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
|
-
*
|
|
85
|
-
*
|
|
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 {
|
|
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;
|