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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/lib/testing/storyboard/default-invariants.js +4 -0
  2. package/dist/lib/testing/storyboard/default-invariants.mjs +4 -0
  3. package/dist/lib/testing/storyboard/loader.js +31 -0
  4. package/dist/lib/testing/storyboard/loader.mjs +31 -0
  5. package/dist/lib/testing/storyboard/pricing-currency-filter.d.mts +13 -0
  6. package/dist/lib/testing/storyboard/pricing-currency-filter.d.ts +13 -0
  7. package/dist/lib/testing/storyboard/pricing-currency-filter.js +175 -0
  8. package/dist/lib/testing/storyboard/pricing-currency-filter.mjs +151 -0
  9. package/dist/lib/testing/storyboard/request-builder.js +6 -3
  10. package/dist/lib/testing/storyboard/request-builder.mjs +6 -3
  11. package/dist/lib/testing/storyboard/required-metric-filters.d.mts +9 -0
  12. package/dist/lib/testing/storyboard/required-metric-filters.d.ts +9 -0
  13. package/dist/lib/testing/storyboard/required-metric-filters.js +128 -0
  14. package/dist/lib/testing/storyboard/required-metric-filters.mjs +104 -0
  15. package/dist/lib/testing/storyboard/runner.js +244 -48
  16. package/dist/lib/testing/storyboard/runner.mjs +246 -49
  17. package/dist/lib/testing/storyboard/seeding.d.mts +86 -1
  18. package/dist/lib/testing/storyboard/seeding.d.ts +86 -1
  19. package/dist/lib/testing/storyboard/seeding.js +91 -18
  20. package/dist/lib/testing/storyboard/seeding.mjs +90 -18
  21. package/dist/lib/testing/storyboard/types.d.mts +30 -1
  22. package/dist/lib/testing/storyboard/types.d.ts +30 -1
  23. package/dist/lib/testing/storyboard/types.js +1 -0
  24. package/dist/lib/testing/storyboard/types.mjs +1 -0
  25. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  26. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  27. package/dist/lib/version.d.mts +3 -3
  28. package/dist/lib/version.d.ts +3 -3
  29. package/dist/lib/version.js +3 -3
  30. package/dist/lib/version.mjs +3 -3
  31. package/docs/TYPE-SUMMARY.md +2 -2
  32. package/docs/guides/WHOLESALE-PAGE-COLLECTION.md +67 -0
  33. package/docs/llms.txt +2 -2
  34. package/docs/migration-14.x-rc-worksheet.md +4 -4
  35. package/package.json +1 -1
@@ -0,0 +1,128 @@
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 required_metric_filters_exports = {};
20
+ __export(required_metric_filters_exports, {
21
+ requiredMetricFiltersAssertion: () => requiredMetricFiltersAssertion
22
+ });
23
+ module.exports = __toCommonJS(required_metric_filters_exports);
24
+ const CONTAINERS = {
25
+ viewable_rate: "viewability",
26
+ viewable_impressions: "viewability",
27
+ measurable_impressions: "viewability",
28
+ viewed_seconds: "viewability",
29
+ quartile_25: "quartile_data",
30
+ quartile_50: "quartile_data",
31
+ quartile_75: "quartile_data",
32
+ quartile_100: "quartile_data"
33
+ };
34
+ const DERIVED_FROM = {
35
+ ctr: ["clicks", "impressions"],
36
+ completion_rate: ["completed_views", "impressions"],
37
+ roas: ["conversion_value", "spend"],
38
+ cost_per_acquisition: ["spend", "conversions"],
39
+ cost_per_click: ["spend", "clicks"],
40
+ cost_per_completed_view: ["spend", "completed_views"],
41
+ cpm: ["spend", "impressions"],
42
+ viewable_rate: ["viewable_impressions", "measurable_impressions"]
43
+ };
44
+ function record(value) {
45
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
46
+ }
47
+ function metricAvailable(metric, declared) {
48
+ if (declared.has(metric)) return true;
49
+ const container = Object.hasOwn(CONTAINERS, metric) ? CONTAINERS[metric] : void 0;
50
+ if (container && declared.has(container)) return true;
51
+ const derivation = Object.hasOwn(DERIVED_FROM, metric) ? DERIVED_FROM[metric] : void 0;
52
+ return derivation?.every((dependency) => metricAvailable(dependency, declared)) ?? false;
53
+ }
54
+ function vendorMetricMatches(actual, required) {
55
+ const entry = record(actual);
56
+ if (!entry || required.metric_id !== void 0 && entry.metric_id !== required.metric_id) return false;
57
+ const vendor = record(required.vendor);
58
+ if (!vendor) return true;
59
+ const actualVendor = record(entry.vendor);
60
+ if (!actualVendor) return false;
61
+ return actualVendor.domain === vendor.domain && (vendor.brand_id === void 0 || actualVendor.brand_id === vendor.brand_id);
62
+ }
63
+ const requiredMetricFiltersAssertion = {
64
+ id: "discovery.required_metric_filters",
65
+ default: true,
66
+ description: "Every returned product satisfies all requested standard and vendor metric filters.",
67
+ onStep(_ctx, step) {
68
+ if (step.skipped || step.task !== "get_products") return [];
69
+ const request = record(step.request?.payload);
70
+ const filters = record(request?.filters);
71
+ const required = Array.isArray(filters?.required_metrics) ? filters.required_metrics.filter((metric) => typeof metric === "string") : [];
72
+ const requiredVendors = Array.isArray(filters?.required_vendor_metrics) ? filters.required_vendor_metrics.flatMap((value) => {
73
+ const entry = record(value);
74
+ return entry && (typeof entry.metric_id === "string" || typeof record(entry.vendor)?.domain === "string") ? [entry] : [];
75
+ }) : [];
76
+ if (required.length === 0 && requiredVendors.length === 0) return [];
77
+ const raw = record(step.response ?? step.response_record?.payload);
78
+ const response = Array.isArray(raw?.products) ? raw : record(raw?.data) ?? raw;
79
+ if (!Array.isArray(response?.products) || response.products.length === 0) return [];
80
+ const reportingProjectedAway = Array.isArray(request?.fields) && !request.fields.includes("reporting_capabilities");
81
+ const findings = [];
82
+ for (const [index, value] of response.products.entries()) {
83
+ const product = record(value);
84
+ if (!product) continue;
85
+ if (reportingProjectedAway && product.reporting_capabilities === void 0) {
86
+ findings.push({
87
+ passed: true,
88
+ status: "not_applicable",
89
+ step_id: step.step_id,
90
+ description: `products[${index}]: required metric filter compatibility is unproven because reporting_capabilities was projected away`
91
+ });
92
+ continue;
93
+ }
94
+ const capabilities = record(product.reporting_capabilities);
95
+ const declared = /* @__PURE__ */ new Set(["impressions", "spend"]);
96
+ if (Array.isArray(capabilities?.available_metrics)) {
97
+ for (const metric of capabilities.available_metrics) if (typeof metric === "string") declared.add(metric);
98
+ }
99
+ const missing = required.filter((metric) => !metricAvailable(metric, declared));
100
+ if (required.length > 0)
101
+ findings.push({
102
+ passed: missing.length === 0,
103
+ step_id: step.step_id,
104
+ description: `products[${index}] must satisfy every filters.required_metrics entry`,
105
+ ...missing.length > 0 && {
106
+ error: `products[${index}] does not declare required metrics ${JSON.stringify(missing)}`
107
+ }
108
+ });
109
+ const vendorMetrics = Array.isArray(capabilities?.vendor_metrics) ? capabilities.vendor_metrics : [];
110
+ for (const [filterIndex, vendor] of requiredVendors.entries()) {
111
+ const matched = vendorMetrics.some((actual) => vendorMetricMatches(actual, vendor));
112
+ findings.push({
113
+ passed: matched,
114
+ step_id: step.step_id,
115
+ description: `products[${index}] must satisfy filters.required_vendor_metrics[${filterIndex}] on one declared vendor metric`,
116
+ ...!matched && {
117
+ error: `products[${index}] has no vendor metric matching filters.required_vendor_metrics[${filterIndex}]`
118
+ }
119
+ });
120
+ }
121
+ }
122
+ return findings;
123
+ }
124
+ };
125
+ // Annotate the CommonJS export names for ESM import in node:
126
+ 0 && (module.exports = {
127
+ requiredMetricFiltersAssertion
128
+ });
@@ -0,0 +1,104 @@
1
+ const CONTAINERS = {
2
+ viewable_rate: "viewability",
3
+ viewable_impressions: "viewability",
4
+ measurable_impressions: "viewability",
5
+ viewed_seconds: "viewability",
6
+ quartile_25: "quartile_data",
7
+ quartile_50: "quartile_data",
8
+ quartile_75: "quartile_data",
9
+ quartile_100: "quartile_data"
10
+ };
11
+ const DERIVED_FROM = {
12
+ ctr: ["clicks", "impressions"],
13
+ completion_rate: ["completed_views", "impressions"],
14
+ roas: ["conversion_value", "spend"],
15
+ cost_per_acquisition: ["spend", "conversions"],
16
+ cost_per_click: ["spend", "clicks"],
17
+ cost_per_completed_view: ["spend", "completed_views"],
18
+ cpm: ["spend", "impressions"],
19
+ viewable_rate: ["viewable_impressions", "measurable_impressions"]
20
+ };
21
+ function record(value) {
22
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
23
+ }
24
+ function metricAvailable(metric, declared) {
25
+ if (declared.has(metric)) return true;
26
+ const container = Object.hasOwn(CONTAINERS, metric) ? CONTAINERS[metric] : void 0;
27
+ if (container && declared.has(container)) return true;
28
+ const derivation = Object.hasOwn(DERIVED_FROM, metric) ? DERIVED_FROM[metric] : void 0;
29
+ return derivation?.every((dependency) => metricAvailable(dependency, declared)) ?? false;
30
+ }
31
+ function vendorMetricMatches(actual, required) {
32
+ const entry = record(actual);
33
+ if (!entry || required.metric_id !== void 0 && entry.metric_id !== required.metric_id) return false;
34
+ const vendor = record(required.vendor);
35
+ if (!vendor) return true;
36
+ const actualVendor = record(entry.vendor);
37
+ if (!actualVendor) return false;
38
+ return actualVendor.domain === vendor.domain && (vendor.brand_id === void 0 || actualVendor.brand_id === vendor.brand_id);
39
+ }
40
+ const requiredMetricFiltersAssertion = {
41
+ id: "discovery.required_metric_filters",
42
+ default: true,
43
+ description: "Every returned product satisfies all requested standard and vendor metric filters.",
44
+ onStep(_ctx, step) {
45
+ if (step.skipped || step.task !== "get_products") return [];
46
+ const request = record(step.request?.payload);
47
+ const filters = record(request?.filters);
48
+ const required = Array.isArray(filters?.required_metrics) ? filters.required_metrics.filter((metric) => typeof metric === "string") : [];
49
+ const requiredVendors = Array.isArray(filters?.required_vendor_metrics) ? filters.required_vendor_metrics.flatMap((value) => {
50
+ const entry = record(value);
51
+ return entry && (typeof entry.metric_id === "string" || typeof record(entry.vendor)?.domain === "string") ? [entry] : [];
52
+ }) : [];
53
+ if (required.length === 0 && requiredVendors.length === 0) return [];
54
+ const raw = record(step.response ?? step.response_record?.payload);
55
+ const response = Array.isArray(raw?.products) ? raw : record(raw?.data) ?? raw;
56
+ if (!Array.isArray(response?.products) || response.products.length === 0) return [];
57
+ const reportingProjectedAway = Array.isArray(request?.fields) && !request.fields.includes("reporting_capabilities");
58
+ const findings = [];
59
+ for (const [index, value] of response.products.entries()) {
60
+ const product = record(value);
61
+ if (!product) continue;
62
+ if (reportingProjectedAway && product.reporting_capabilities === void 0) {
63
+ findings.push({
64
+ passed: true,
65
+ status: "not_applicable",
66
+ step_id: step.step_id,
67
+ description: `products[${index}]: required metric filter compatibility is unproven because reporting_capabilities was projected away`
68
+ });
69
+ continue;
70
+ }
71
+ const capabilities = record(product.reporting_capabilities);
72
+ const declared = /* @__PURE__ */ new Set(["impressions", "spend"]);
73
+ if (Array.isArray(capabilities?.available_metrics)) {
74
+ for (const metric of capabilities.available_metrics) if (typeof metric === "string") declared.add(metric);
75
+ }
76
+ const missing = required.filter((metric) => !metricAvailable(metric, declared));
77
+ if (required.length > 0)
78
+ findings.push({
79
+ passed: missing.length === 0,
80
+ step_id: step.step_id,
81
+ description: `products[${index}] must satisfy every filters.required_metrics entry`,
82
+ ...missing.length > 0 && {
83
+ error: `products[${index}] does not declare required metrics ${JSON.stringify(missing)}`
84
+ }
85
+ });
86
+ const vendorMetrics = Array.isArray(capabilities?.vendor_metrics) ? capabilities.vendor_metrics : [];
87
+ for (const [filterIndex, vendor] of requiredVendors.entries()) {
88
+ const matched = vendorMetrics.some((actual) => vendorMetricMatches(actual, vendor));
89
+ findings.push({
90
+ passed: matched,
91
+ step_id: step.step_id,
92
+ description: `products[${index}] must satisfy filters.required_vendor_metrics[${filterIndex}] on one declared vendor metric`,
93
+ ...!matched && {
94
+ error: `products[${index}] has no vendor metric matching filters.required_vendor_metrics[${filterIndex}]`
95
+ }
96
+ });
97
+ }
98
+ }
99
+ return findings;
100
+ }
101
+ };
102
+ export {
103
+ requiredMetricFiltersAssertion
104
+ };
@@ -1145,6 +1145,7 @@ function buildPhaseCapabilitySkippedSteps(storyboard, phase, detail, context) {
1145
1145
  }
1146
1146
  const REQUIREMENT_TO_SKIP_REASON = {
1147
1147
  controller: "missing_test_controller",
1148
+ collect_product_pages_runner: "requirement_unmet",
1148
1149
  seeded_state: "requirement_unmet",
1149
1150
  real_wire: "requirement_unmet",
1150
1151
  webhook_receiver: "requirement_unmet",
@@ -1278,6 +1279,8 @@ async function checkRequires(requires, storyboard, options, profile, precomputed
1278
1279
  continue;
1279
1280
  }
1280
1281
  switch (requirement) {
1282
+ case "collect_product_pages_runner":
1283
+ break;
1281
1284
  case "controller": {
1282
1285
  if (!options.agentTools) continue;
1283
1286
  if (!options.agentTools.includes("comply_test_controller")) {
@@ -3511,6 +3514,11 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
3511
3514
  }
3512
3515
  assignment ??= dispatch.nextFor(step);
3513
3516
  const stepExecutionState = buildExecutionState(assignment.agentUrl, assignment.profile);
3517
+ stepExecutionState.onCollectedProductPage = async (page, pageIndex) => {
3518
+ const observed = await applyStepAssertions(step, page, assertions, assertionContexts, pageIndex);
3519
+ assertionResults.push(...observed);
3520
+ if (observed.some((assertion) => !assertion.passed)) assertionsFailed = true;
3521
+ };
3514
3522
  if (routingContext) {
3515
3523
  let probes = routedPriorProbes.get(assignment.instanceIndex);
3516
3524
  if (!probes) {
@@ -3628,45 +3636,10 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
3628
3636
  }
3629
3637
  stepResults.push(result2);
3630
3638
  priorStepResults.set(step.id, result2);
3631
- const schemaInvalidResponse = result2.validations.some(
3632
- (v) => v.check === "response_schema" && (0, import_validations.validationFailsStep)(v)
3633
- );
3634
- const rateLimitTripNotApplicable = result2.skipped === true && result2.skip_reason === "rate_limit_not_triggered";
3635
- for (const spec of assertions) {
3636
- if (!spec.onStep) continue;
3637
- if ((0, import_assertions.stepDisablesAssertion)(step.invariants, spec.id)) continue;
3638
- if (schemaInvalidResponse) {
3639
- result2.validations.push({
3640
- check: "assertion",
3641
- passed: true,
3642
- description: `${spec.id}: skipped \u2014 response failed schema validation (adcp-client#1709)`
3643
- });
3644
- continue;
3645
- }
3646
- const raw = await spec.onStep(assertionContexts.get(spec.id), {
3647
- ...result2,
3648
- context: withoutProtectedCaptures(result2.context ?? {})
3649
- });
3650
- for (const r of raw) {
3651
- const full = { ...r, assertion_id: spec.id, scope: "step", step_id: step.id };
3652
- assertionResults.push(full);
3653
- if (!rateLimitTripNotApplicable) {
3654
- result2.validations.push({
3655
- check: "assertion",
3656
- passed: r.passed,
3657
- description: `${spec.id}: ${r.description}`,
3658
- ...r.error !== void 0 && { error: r.error }
3659
- });
3660
- }
3661
- if (r.hint) {
3662
- const existing = result2.hints ?? [];
3663
- result2.hints = [...existing, r.hint];
3664
- }
3665
- if (!r.passed) {
3666
- if (!rateLimitTripNotApplicable) result2.passed = false;
3667
- assertionsFailed = true;
3668
- }
3669
- }
3639
+ if (!result2.product_page_collection) {
3640
+ const observedAssertions = await applyStepAssertions(step, result2, assertions, assertionContexts);
3641
+ assertionResults.push(...observedAssertions);
3642
+ if (observedAssertions.some((assertion) => !assertion.passed)) assertionsFailed = true;
3670
3643
  }
3671
3644
  if (step.task === "protected_resource_metadata") {
3672
3645
  if (result2.skipped && result2.skip_reason === "oauth_not_advertised") {
@@ -4848,6 +4821,18 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
4848
4821
  }
4849
4822
  }
4850
4823
  }
4824
+ if (found.step.collect_product_pages) {
4825
+ const pageAssertions = (0, import_assertions.resolveAssertions)(storyboard.invariants);
4826
+ const pageContexts = /* @__PURE__ */ new Map();
4827
+ for (const spec of pageAssertions) {
4828
+ const assertionContext = { storyboard, agentUrl, options, state: {} };
4829
+ pageContexts.set(spec.id, assertionContext);
4830
+ if (spec.onStart) await spec.onStart(assertionContext);
4831
+ }
4832
+ runState.onCollectedProductPage = async (page, pageIndex) => {
4833
+ await applyStepAssertions(found.step, page, pageAssertions, pageContexts, pageIndex);
4834
+ };
4835
+ }
4851
4836
  const result = prerequisiteFailure ?? await executeStep(client, found.step, storyboard.id, found.phaseId, context, allSteps, options, runState);
4852
4837
  if (!result.skipped && result.passed && found.step.contributes_to) {
4853
4838
  if (evalContributesIf(found.step.contributes_if, /* @__PURE__ */ new Map())) {
@@ -4866,6 +4851,82 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
4866
4851
  ]);
4867
4852
  }
4868
4853
  }
4854
+ async function applyStepAssertions(step, result, assertions, assertionContexts, pageIndex) {
4855
+ const findings = [];
4856
+ const schemaInvalid = result.validations.some((v) => v.check === "response_schema" && (0, import_validations.validationFailsStep)(v));
4857
+ const rateSkip = result.skipped === true && result.skip_reason === "rate_limit_not_triggered";
4858
+ for (const spec of assertions) {
4859
+ if (!spec.onStep || (0, import_assertions.stepDisablesAssertion)(step.invariants, spec.id)) continue;
4860
+ if (schemaInvalid) {
4861
+ result.validations.push({
4862
+ check: "assertion",
4863
+ passed: true,
4864
+ description: `${spec.id}: skipped \u2014 response failed schema validation (adcp-client#1709)`
4865
+ });
4866
+ continue;
4867
+ }
4868
+ const raw = await spec.onStep(assertionContexts.get(spec.id), {
4869
+ ...result,
4870
+ context: withoutProtectedCaptures(result.context ?? {})
4871
+ });
4872
+ for (const observed of raw) {
4873
+ const description = `${pageIndex === void 0 ? "" : `[physical page ${pageIndex}] `}${observed.description}`;
4874
+ findings.push({ ...observed, description, assertion_id: spec.id, scope: "step", step_id: step.id });
4875
+ if (!rateSkip)
4876
+ result.validations.push({
4877
+ check: "assertion",
4878
+ passed: observed.passed,
4879
+ description: `${spec.id}: ${description}`,
4880
+ ...observed.error !== void 0 && { error: observed.error },
4881
+ ...pageIndex !== void 0 && !observed.passed && { request: result.request, response: result.response_record ?? null }
4882
+ });
4883
+ if (observed.hint) result.hints = [...result.hints ?? [], observed.hint];
4884
+ if (!observed.passed && !rateSkip) result.passed = false;
4885
+ }
4886
+ }
4887
+ return findings;
4888
+ }
4889
+ function productPageStepResult(step, phaseId, context, page, schemaVersion) {
4890
+ const failedSchema = page.schemaFailure;
4891
+ return {
4892
+ step_id: step.id,
4893
+ phase_id: phaseId,
4894
+ task: "get_products",
4895
+ title: step.title,
4896
+ passed: page.schema_validated && page.result?.success === true,
4897
+ duration_ms: page.response?.duration_ms ?? 0,
4898
+ request: page.request,
4899
+ ...page.response && { response_record: page.response },
4900
+ response: (0, import_redact_secrets.redactSecrets)(page.result?.data),
4901
+ extraction: page.extraction,
4902
+ context,
4903
+ validations: failedSchema ? [
4904
+ {
4905
+ check: "response_schema",
4906
+ passed: false,
4907
+ description: `Physical product page ${page.page_index} satisfies the get_products response schema`,
4908
+ error: failedSchema.detail,
4909
+ json_pointer: null,
4910
+ expected: failedSchema.schema_id,
4911
+ actual: "schema_validation_failed",
4912
+ schema_id: failedSchema.schema_id,
4913
+ schema_url: failedSchema.schema_url,
4914
+ request: page.request,
4915
+ response: page.response,
4916
+ strict: { valid: false, variant: "sync" }
4917
+ }
4918
+ ] : page.schema_validated ? [
4919
+ {
4920
+ check: "response_schema",
4921
+ passed: true,
4922
+ description: `Physical product page ${page.page_index} satisfies the get_products response schema`,
4923
+ schema_id: `/schemas/${schemaVersion}/media-buy/get-products-response.json`,
4924
+ schema_url: `https://adcontextprotocol.org/schemas/${schemaVersion}/media-buy/get-products-response.json`,
4925
+ ...page.strict_schema_validated && { strict: { valid: true, variant: "sync" } }
4926
+ }
4927
+ ] : []
4928
+ };
4929
+ }
4869
4930
  function unresolvedStepContextVars(step, request, context, runState) {
4870
4931
  const unavailableInputs = (step.context_inputs ?? []).filter(
4871
4932
  (input) => !Object.hasOwn(context, input.key) && (runState.capabilityUnavailableContextKeys?.has(input.key) === true || runState.missingPrerequisiteContextKeys?.has(input.key) === true)
@@ -5318,6 +5379,34 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
5318
5379
  };
5319
5380
  }
5320
5381
  const useRawProbe = rawProbeHeaders !== void 0 && !useSdkAuthOverride;
5382
+ if (step.collect_product_pages && (useRawProbe || request.buying_mode !== "wholesale" || Object.hasOwn(request, "idempotency_key") || request.pagination !== null && typeof request.pagination === "object" && "cursor" in request.pagination || ["if_none_match", "if_wholesale_feed_version", "if_pricing_version"].some((field) => request[field] !== void 0))) {
5383
+ const detail = "collect_product_pages requires fresh unkeyed reads and an unconditional wholesale first page through the official SDK client";
5384
+ return {
5385
+ step_id: step.id,
5386
+ phase_id: phaseId,
5387
+ task: step.task,
5388
+ title: step.title,
5389
+ passed: false,
5390
+ duration_ms: 0,
5391
+ context,
5392
+ extraction: { path: "none" },
5393
+ error: detail,
5394
+ validations: [
5395
+ {
5396
+ check: "product_page_collection",
5397
+ passed: false,
5398
+ description: detail,
5399
+ json_pointer: null,
5400
+ expected: "unkeyed unconditional wholesale query",
5401
+ actual: (0, import_redact_secrets.redactSecrets)(request),
5402
+ schema_id: null,
5403
+ schema_url: null,
5404
+ request: null,
5405
+ response: null
5406
+ }
5407
+ ]
5408
+ };
5409
+ }
5321
5410
  const stepClient = () => authOverrideClient ?? client;
5322
5411
  let taskResult;
5323
5412
  let stepResult;
@@ -5327,6 +5416,10 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
5327
5416
  let requestUrl;
5328
5417
  let a2aEnvelope;
5329
5418
  let crossResponses;
5419
+ let productPageCollection;
5420
+ let collectedPageValidations = [];
5421
+ let actualCollectedResponse;
5422
+ let collectedRequestRecord;
5330
5423
  if (step.parallel_dispatch && !useRawProbe) {
5331
5424
  const contractsInScope = new Set(options.contracts ?? []);
5332
5425
  if (!contractsInScope.has(import_parallel_dispatch.PARALLEL_DISPATCH_CONTRACT)) {
@@ -5495,7 +5588,96 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
5495
5588
  } else {
5496
5589
  const captureA2a = options.protocol === "a2a";
5497
5590
  let a2aCaptures;
5498
- if (step.parallel_dispatch) {
5591
+ if (step.collect_product_pages) {
5592
+ const started = Date.now();
5593
+ const pages = [];
5594
+ const pageValidations = [];
5595
+ let pagesPassed = true;
5596
+ const collection = await (0, import_seeding.collectWholesaleProductPages)(stepClient(), options, request, {
5597
+ retainRestartEvidence: true,
5598
+ dispatch: (pageRequest) => (0, import_task_map.executeStoryboardTask)(stepClient(), effectiveStep.task, pageRequest, {
5599
+ skipIdempotencyAutoInject: testsIdempotencyKeyOmission,
5600
+ skipAccountValidation: testsMissingAccount,
5601
+ skipRequestValidation: testsSchemaInvalidRequest,
5602
+ preserveGovernedPayload: governedRequest,
5603
+ responseProjection,
5604
+ mediaBuyLifecycleCompatibility: options.mediaBuyLifecycleCompatibility,
5605
+ signal: options.signal
5606
+ }),
5607
+ onPage: async (observation) => {
5608
+ const physical = productPageStepResult(
5609
+ step,
5610
+ phaseId,
5611
+ context,
5612
+ observation,
5613
+ options._serverAdcpVersion ?? options.adcpVersion ?? import_version.ADCP_VERSION
5614
+ );
5615
+ await runState.onCollectedProductPage?.(physical, observation.page_index);
5616
+ pagesPassed &&= physical.passed;
5617
+ pageValidations.push(...physical.validations);
5618
+ const page = {
5619
+ page_index: observation.page_index,
5620
+ restart_count: observation.restart_count,
5621
+ request: observation.request,
5622
+ response: observation.response,
5623
+ extraction: observation.extraction,
5624
+ schema_validated: observation.schema_validated
5625
+ };
5626
+ if (pages.length >= 16 && page.response) {
5627
+ page.response = {
5628
+ ...page.response,
5629
+ payload: {
5630
+ response_not_retained: "Physical page was checked; response evidence exceeded the 16-page retention budget."
5631
+ }
5632
+ };
5633
+ }
5634
+ pages.push(page);
5635
+ }
5636
+ });
5637
+ if (options.signal?.aborted) options.signal.throwIfAborted();
5638
+ const logical = collection.ok ? {
5639
+ ...collection.catalog.firstPage.result.data,
5640
+ products: collection.catalog.products,
5641
+ ...collection.catalog.errors.length > 0 && { errors: collection.catalog.errors }
5642
+ } : void 0;
5643
+ if (logical) delete logical.pagination;
5644
+ productPageCollection = {
5645
+ complete: collection.ok,
5646
+ page_count: pages.length,
5647
+ restart_count: pages.reduce((maximum, page) => Math.max(maximum, page.restart_count), 0),
5648
+ pages,
5649
+ ...logical && { logical_response: (0, import_redact_secrets.redactSecrets)(logical) }
5650
+ };
5651
+ collectedRequestRecord = collection.ok ? collection.catalog.firstPage.request : collection.exchange?.request;
5652
+ const observedResponse = collection.ok ? collection.catalog.firstPage.response : collection.exchange?.response;
5653
+ responseRecord = observedResponse ?? void 0;
5654
+ actualCollectedResponse = collection.ok ? collection.catalog.firstPage.result.data : collection.exchange?.response?.payload;
5655
+ taskResult = collection.ok ? { ...collection.catalog.firstPage.result, data: logical } : {
5656
+ success: false,
5657
+ error: collection.error.replace(/^fixture discovery/, "Product page collection")
5658
+ };
5659
+ stepResult = {
5660
+ duration_ms: Date.now() - started,
5661
+ passed: collection.ok && pagesPassed,
5662
+ ...!collection.ok && { error: collection.error.replace(/^fixture discovery/, "Product page collection") }
5663
+ };
5664
+ collectedPageValidations = pageValidations;
5665
+ if (!collection.ok && !pageValidations.some((validation) => (0, import_validations.validationFailsStep)(validation))) {
5666
+ collectedPageValidations.push({
5667
+ check: "product_page_collection",
5668
+ passed: false,
5669
+ description: "Wholesale collection completes one stable, duplicate-free feed",
5670
+ error: collection.error,
5671
+ json_pointer: null,
5672
+ expected: "a complete stable wholesale collection",
5673
+ actual: collection.error,
5674
+ schema_id: null,
5675
+ schema_url: null,
5676
+ ...collection.exchange?.request && { request: collection.exchange.request },
5677
+ response: collection.exchange?.response ?? null
5678
+ });
5679
+ }
5680
+ } else if (step.parallel_dispatch) {
5499
5681
  const started = Date.now();
5500
5682
  crossResponses = await (0, import_parallel_dispatch.runParallelDispatches)(stepClient(), effectiveStep.task, request, {
5501
5683
  spec: step.parallel_dispatch,
@@ -5583,7 +5765,7 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
5583
5765
  const requestRecord = {
5584
5766
  transport: options.protocol === "a2a" ? "a2a" : "mcp",
5585
5767
  operation: effectiveStep.task,
5586
- payload: (0, import_redact_secrets.redactSecrets)(request),
5768
+ payload: collectedRequestRecord?.payload ?? (0, import_redact_secrets.redactSecrets)(request),
5587
5769
  ...effectiveRequestUrl ? { url: (0, import_oauth_metadata_graph.redactOAuthUrlForOutput)(effectiveRequestUrl) } : {}
5588
5770
  };
5589
5771
  const inputSchemaStripNotices = collectInputSchemaFieldStripNotices(
@@ -5771,9 +5953,9 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
5771
5953
  success: true,
5772
5954
  data: schemaValidationError.data
5773
5955
  } : void 0);
5774
- let validations = [];
5956
+ let validations = [...collectedPageValidations];
5775
5957
  if (step.validations?.length && (validationTaskResult || httpResult)) {
5776
- const resolvedValidations = step.validations.map((v) => {
5958
+ const resolvedValidations = step.validations.filter((v) => !productPageCollection || v.check !== "response_schema").map((v) => {
5777
5959
  const resolved = { ...v };
5778
5960
  if (resolved.value !== void 0) {
5779
5961
  resolved.value = (0, import_context.injectContext)({ __v: resolved.value }, context, runState.runnerVars).__v;
@@ -5830,9 +6012,11 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
5830
6012
  };
5831
6013
  })()
5832
6014
  };
5833
- validations = (0, import_validations.runValidations)(
5834
- resolvedValidations.filter((validation) => validation.check !== "property_list_reference"),
5835
- vctx
6015
+ validations.push(
6016
+ ...(0, import_validations.runValidations)(
6017
+ resolvedValidations.filter((validation) => validation.check !== "property_list_reference"),
6018
+ vctx
6019
+ )
5836
6020
  );
5837
6021
  for (const validation of resolvedValidations.filter((validation2) => validation2.check === "property_list_reference")) {
5838
6022
  const result = effectiveStep.task !== "create_property_list" || !validationTaskResult?.success ? {
@@ -6102,6 +6286,14 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
6102
6286
  }
6103
6287
  }
6104
6288
  }
6289
+ if (productPageCollection) {
6290
+ for (const validation of validations) {
6291
+ if (collectedPageValidations.includes(validation)) continue;
6292
+ if (validation.json_pointer)
6293
+ validation.json_pointer = `/product_page_collection/logical_response${validation.json_pointer}`;
6294
+ validation.description = `[logical product collection] ${validation.description ?? validation.check}`;
6295
+ }
6296
+ }
6105
6297
  if (protectedViolations.length > 0) validations.push(...protectedViolations);
6106
6298
  const allValidationsPassedFinal = validations.every((v) => !(0, import_validations.validationFailsStep)(v));
6107
6299
  const stepFailed = !(passed && allValidationsPassedFinal);
@@ -6131,7 +6323,8 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
6131
6323
  // Legacy `response` field (new code reads `response_record`).
6132
6324
  // Redact in case a downstream consumer still keys off it; the
6133
6325
  // modern `response_record.payload` path is already redacted.
6134
- response: (0, import_redact_secrets.redactSecrets)(taskResult?.data),
6326
+ response: (0, import_redact_secrets.redactSecrets)(productPageCollection ? actualCollectedResponse : taskResult?.data),
6327
+ ...productPageCollection && { product_page_collection: productPageCollection },
6135
6328
  validations,
6136
6329
  // NOT projected here: the full runner threads this map from step to
6137
6330
  // step, so stripping the reserved namespace would break the very
@@ -6155,7 +6348,10 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
6155
6348
  next,
6156
6349
  request: requestRecord,
6157
6350
  ...responseRecord && { response_record: responseRecord },
6158
- extraction: extractionFromTaskResult(taskResult),
6351
+ extraction: productPageCollection ? {
6352
+ path: "none",
6353
+ note: "Authored checks and context use product_page_collection.logical_response; response_record remains an actual physical page."
6354
+ } : extractionFromTaskResult(taskResult),
6159
6355
  ...inputSchemaStripNotices.length > 0 && { notices: inputSchemaStripNotices },
6160
6356
  ...hints.length > 0 && { hints }
6161
6357
  };