@adcp/sdk 17.0.0-beta.6 → 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 (33) 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/required-metric-filters.d.mts +9 -0
  10. package/dist/lib/testing/storyboard/required-metric-filters.d.ts +9 -0
  11. package/dist/lib/testing/storyboard/required-metric-filters.js +128 -0
  12. package/dist/lib/testing/storyboard/required-metric-filters.mjs +104 -0
  13. package/dist/lib/testing/storyboard/runner.js +244 -48
  14. package/dist/lib/testing/storyboard/runner.mjs +246 -49
  15. package/dist/lib/testing/storyboard/seeding.d.mts +86 -1
  16. package/dist/lib/testing/storyboard/seeding.d.ts +86 -1
  17. package/dist/lib/testing/storyboard/seeding.js +91 -18
  18. package/dist/lib/testing/storyboard/seeding.mjs +90 -18
  19. package/dist/lib/testing/storyboard/types.d.mts +30 -1
  20. package/dist/lib/testing/storyboard/types.d.ts +30 -1
  21. package/dist/lib/testing/storyboard/types.js +1 -0
  22. package/dist/lib/testing/storyboard/types.mjs +1 -0
  23. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  24. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  25. package/dist/lib/version.d.mts +3 -3
  26. package/dist/lib/version.d.ts +3 -3
  27. package/dist/lib/version.js +3 -3
  28. package/dist/lib/version.mjs +3 -3
  29. package/docs/TYPE-SUMMARY.md +2 -2
  30. package/docs/guides/WHOLESALE-PAGE-COLLECTION.md +67 -0
  31. package/docs/llms.txt +2 -2
  32. package/docs/migration-14.x-rc-worksheet.md +4 -4
  33. package/package.json +1 -1
@@ -132,7 +132,8 @@ import { REQUEST_SIGNING_PROBE_TASK } from "./request-signing/synthesize.mjs";
132
132
  import { createWebhookReceiver } from "./webhook-receiver.mjs";
133
133
  import { WEBHOOK_ASSERTION_TASKS, armWebhookAssertions, executeWebhookAssertionStep } from "./webhook-assertions.mjs";
134
134
  import {
135
- runControllerSeeding
135
+ runControllerSeeding,
136
+ collectWholesaleProductPages
136
137
  } from "./seeding.mjs";
137
138
  import { callControllerRaw } from "../test-controller.mjs";
138
139
  import { applyFixtureBindingsToRequest } from "./fixture-resolution.mjs";
@@ -1187,6 +1188,7 @@ function buildPhaseCapabilitySkippedSteps(storyboard, phase, detail, context) {
1187
1188
  }
1188
1189
  const REQUIREMENT_TO_SKIP_REASON = {
1189
1190
  controller: "missing_test_controller",
1191
+ collect_product_pages_runner: "requirement_unmet",
1190
1192
  seeded_state: "requirement_unmet",
1191
1193
  real_wire: "requirement_unmet",
1192
1194
  webhook_receiver: "requirement_unmet",
@@ -1320,6 +1322,8 @@ async function checkRequires(requires, storyboard, options, profile, precomputed
1320
1322
  continue;
1321
1323
  }
1322
1324
  switch (requirement) {
1325
+ case "collect_product_pages_runner":
1326
+ break;
1323
1327
  case "controller": {
1324
1328
  if (!options.agentTools) continue;
1325
1329
  if (!options.agentTools.includes("comply_test_controller")) {
@@ -3553,6 +3557,11 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
3553
3557
  }
3554
3558
  assignment ??= dispatch.nextFor(step);
3555
3559
  const stepExecutionState = buildExecutionState(assignment.agentUrl, assignment.profile);
3560
+ stepExecutionState.onCollectedProductPage = async (page, pageIndex) => {
3561
+ const observed = await applyStepAssertions(step, page, assertions, assertionContexts, pageIndex);
3562
+ assertionResults.push(...observed);
3563
+ if (observed.some((assertion) => !assertion.passed)) assertionsFailed = true;
3564
+ };
3556
3565
  if (routingContext) {
3557
3566
  let probes = routedPriorProbes.get(assignment.instanceIndex);
3558
3567
  if (!probes) {
@@ -3670,45 +3679,10 @@ async function executeStoryboardPass(agentUrls, storyboard, options, dispatchOff
3670
3679
  }
3671
3680
  stepResults.push(result2);
3672
3681
  priorStepResults.set(step.id, result2);
3673
- const schemaInvalidResponse = result2.validations.some(
3674
- (v) => v.check === "response_schema" && validationFailsStep(v)
3675
- );
3676
- const rateLimitTripNotApplicable = result2.skipped === true && result2.skip_reason === "rate_limit_not_triggered";
3677
- for (const spec of assertions) {
3678
- if (!spec.onStep) continue;
3679
- if (stepDisablesAssertion(step.invariants, spec.id)) continue;
3680
- if (schemaInvalidResponse) {
3681
- result2.validations.push({
3682
- check: "assertion",
3683
- passed: true,
3684
- description: `${spec.id}: skipped \u2014 response failed schema validation (adcp-client#1709)`
3685
- });
3686
- continue;
3687
- }
3688
- const raw = await spec.onStep(assertionContexts.get(spec.id), {
3689
- ...result2,
3690
- context: withoutProtectedCaptures(result2.context ?? {})
3691
- });
3692
- for (const r of raw) {
3693
- const full = { ...r, assertion_id: spec.id, scope: "step", step_id: step.id };
3694
- assertionResults.push(full);
3695
- if (!rateLimitTripNotApplicable) {
3696
- result2.validations.push({
3697
- check: "assertion",
3698
- passed: r.passed,
3699
- description: `${spec.id}: ${r.description}`,
3700
- ...r.error !== void 0 && { error: r.error }
3701
- });
3702
- }
3703
- if (r.hint) {
3704
- const existing = result2.hints ?? [];
3705
- result2.hints = [...existing, r.hint];
3706
- }
3707
- if (!r.passed) {
3708
- if (!rateLimitTripNotApplicable) result2.passed = false;
3709
- assertionsFailed = true;
3710
- }
3711
- }
3682
+ if (!result2.product_page_collection) {
3683
+ const observedAssertions = await applyStepAssertions(step, result2, assertions, assertionContexts);
3684
+ assertionResults.push(...observedAssertions);
3685
+ if (observedAssertions.some((assertion) => !assertion.passed)) assertionsFailed = true;
3712
3686
  }
3713
3687
  if (step.task === "protected_resource_metadata") {
3714
3688
  if (result2.skipped && result2.skip_reason === "oauth_not_advertised") {
@@ -4890,6 +4864,18 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
4890
4864
  }
4891
4865
  }
4892
4866
  }
4867
+ if (found.step.collect_product_pages) {
4868
+ const pageAssertions = resolveAssertions(storyboard.invariants);
4869
+ const pageContexts = /* @__PURE__ */ new Map();
4870
+ for (const spec of pageAssertions) {
4871
+ const assertionContext = { storyboard, agentUrl, options, state: {} };
4872
+ pageContexts.set(spec.id, assertionContext);
4873
+ if (spec.onStart) await spec.onStart(assertionContext);
4874
+ }
4875
+ runState.onCollectedProductPage = async (page, pageIndex) => {
4876
+ await applyStepAssertions(found.step, page, pageAssertions, pageContexts, pageIndex);
4877
+ };
4878
+ }
4893
4879
  const result = prerequisiteFailure ?? await executeStep(client, found.step, storyboard.id, found.phaseId, context, allSteps, options, runState);
4894
4880
  if (!result.skipped && result.passed && found.step.contributes_to) {
4895
4881
  if (evalContributesIf(found.step.contributes_if, /* @__PURE__ */ new Map())) {
@@ -4908,6 +4894,82 @@ async function runStoryboardStepBody(agentUrl, storyboard, stepId, options) {
4908
4894
  ]);
4909
4895
  }
4910
4896
  }
4897
+ async function applyStepAssertions(step, result, assertions, assertionContexts, pageIndex) {
4898
+ const findings = [];
4899
+ const schemaInvalid = result.validations.some((v) => v.check === "response_schema" && validationFailsStep(v));
4900
+ const rateSkip = result.skipped === true && result.skip_reason === "rate_limit_not_triggered";
4901
+ for (const spec of assertions) {
4902
+ if (!spec.onStep || stepDisablesAssertion(step.invariants, spec.id)) continue;
4903
+ if (schemaInvalid) {
4904
+ result.validations.push({
4905
+ check: "assertion",
4906
+ passed: true,
4907
+ description: `${spec.id}: skipped \u2014 response failed schema validation (adcp-client#1709)`
4908
+ });
4909
+ continue;
4910
+ }
4911
+ const raw = await spec.onStep(assertionContexts.get(spec.id), {
4912
+ ...result,
4913
+ context: withoutProtectedCaptures(result.context ?? {})
4914
+ });
4915
+ for (const observed of raw) {
4916
+ const description = `${pageIndex === void 0 ? "" : `[physical page ${pageIndex}] `}${observed.description}`;
4917
+ findings.push({ ...observed, description, assertion_id: spec.id, scope: "step", step_id: step.id });
4918
+ if (!rateSkip)
4919
+ result.validations.push({
4920
+ check: "assertion",
4921
+ passed: observed.passed,
4922
+ description: `${spec.id}: ${description}`,
4923
+ ...observed.error !== void 0 && { error: observed.error },
4924
+ ...pageIndex !== void 0 && !observed.passed && { request: result.request, response: result.response_record ?? null }
4925
+ });
4926
+ if (observed.hint) result.hints = [...result.hints ?? [], observed.hint];
4927
+ if (!observed.passed && !rateSkip) result.passed = false;
4928
+ }
4929
+ }
4930
+ return findings;
4931
+ }
4932
+ function productPageStepResult(step, phaseId, context, page, schemaVersion) {
4933
+ const failedSchema = page.schemaFailure;
4934
+ return {
4935
+ step_id: step.id,
4936
+ phase_id: phaseId,
4937
+ task: "get_products",
4938
+ title: step.title,
4939
+ passed: page.schema_validated && page.result?.success === true,
4940
+ duration_ms: page.response?.duration_ms ?? 0,
4941
+ request: page.request,
4942
+ ...page.response && { response_record: page.response },
4943
+ response: redactSecrets(page.result?.data),
4944
+ extraction: page.extraction,
4945
+ context,
4946
+ validations: failedSchema ? [
4947
+ {
4948
+ check: "response_schema",
4949
+ passed: false,
4950
+ description: `Physical product page ${page.page_index} satisfies the get_products response schema`,
4951
+ error: failedSchema.detail,
4952
+ json_pointer: null,
4953
+ expected: failedSchema.schema_id,
4954
+ actual: "schema_validation_failed",
4955
+ schema_id: failedSchema.schema_id,
4956
+ schema_url: failedSchema.schema_url,
4957
+ request: page.request,
4958
+ response: page.response,
4959
+ strict: { valid: false, variant: "sync" }
4960
+ }
4961
+ ] : page.schema_validated ? [
4962
+ {
4963
+ check: "response_schema",
4964
+ passed: true,
4965
+ description: `Physical product page ${page.page_index} satisfies the get_products response schema`,
4966
+ schema_id: `/schemas/${schemaVersion}/media-buy/get-products-response.json`,
4967
+ schema_url: `https://adcontextprotocol.org/schemas/${schemaVersion}/media-buy/get-products-response.json`,
4968
+ ...page.strict_schema_validated && { strict: { valid: true, variant: "sync" } }
4969
+ }
4970
+ ] : []
4971
+ };
4972
+ }
4911
4973
  function unresolvedStepContextVars(step, request, context, runState) {
4912
4974
  const unavailableInputs = (step.context_inputs ?? []).filter(
4913
4975
  (input) => !Object.hasOwn(context, input.key) && (runState.capabilityUnavailableContextKeys?.has(input.key) === true || runState.missingPrerequisiteContextKeys?.has(input.key) === true)
@@ -5360,6 +5422,34 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
5360
5422
  };
5361
5423
  }
5362
5424
  const useRawProbe = rawProbeHeaders !== void 0 && !useSdkAuthOverride;
5425
+ 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))) {
5426
+ const detail = "collect_product_pages requires fresh unkeyed reads and an unconditional wholesale first page through the official SDK client";
5427
+ return {
5428
+ step_id: step.id,
5429
+ phase_id: phaseId,
5430
+ task: step.task,
5431
+ title: step.title,
5432
+ passed: false,
5433
+ duration_ms: 0,
5434
+ context,
5435
+ extraction: { path: "none" },
5436
+ error: detail,
5437
+ validations: [
5438
+ {
5439
+ check: "product_page_collection",
5440
+ passed: false,
5441
+ description: detail,
5442
+ json_pointer: null,
5443
+ expected: "unkeyed unconditional wholesale query",
5444
+ actual: redactSecrets(request),
5445
+ schema_id: null,
5446
+ schema_url: null,
5447
+ request: null,
5448
+ response: null
5449
+ }
5450
+ ]
5451
+ };
5452
+ }
5363
5453
  const stepClient = () => authOverrideClient ?? client;
5364
5454
  let taskResult;
5365
5455
  let stepResult;
@@ -5369,6 +5459,10 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
5369
5459
  let requestUrl;
5370
5460
  let a2aEnvelope;
5371
5461
  let crossResponses;
5462
+ let productPageCollection;
5463
+ let collectedPageValidations = [];
5464
+ let actualCollectedResponse;
5465
+ let collectedRequestRecord;
5372
5466
  if (step.parallel_dispatch && !useRawProbe) {
5373
5467
  const contractsInScope = new Set(options.contracts ?? []);
5374
5468
  if (!contractsInScope.has(PARALLEL_DISPATCH_CONTRACT)) {
@@ -5537,7 +5631,96 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
5537
5631
  } else {
5538
5632
  const captureA2a = options.protocol === "a2a";
5539
5633
  let a2aCaptures;
5540
- if (step.parallel_dispatch) {
5634
+ if (step.collect_product_pages) {
5635
+ const started = Date.now();
5636
+ const pages = [];
5637
+ const pageValidations = [];
5638
+ let pagesPassed = true;
5639
+ const collection = await collectWholesaleProductPages(stepClient(), options, request, {
5640
+ retainRestartEvidence: true,
5641
+ dispatch: (pageRequest) => executeStoryboardTask(stepClient(), effectiveStep.task, pageRequest, {
5642
+ skipIdempotencyAutoInject: testsIdempotencyKeyOmission,
5643
+ skipAccountValidation: testsMissingAccount,
5644
+ skipRequestValidation: testsSchemaInvalidRequest,
5645
+ preserveGovernedPayload: governedRequest,
5646
+ responseProjection,
5647
+ mediaBuyLifecycleCompatibility: options.mediaBuyLifecycleCompatibility,
5648
+ signal: options.signal
5649
+ }),
5650
+ onPage: async (observation) => {
5651
+ const physical = productPageStepResult(
5652
+ step,
5653
+ phaseId,
5654
+ context,
5655
+ observation,
5656
+ options._serverAdcpVersion ?? options.adcpVersion ?? ADCP_VERSION
5657
+ );
5658
+ await runState.onCollectedProductPage?.(physical, observation.page_index);
5659
+ pagesPassed &&= physical.passed;
5660
+ pageValidations.push(...physical.validations);
5661
+ const page = {
5662
+ page_index: observation.page_index,
5663
+ restart_count: observation.restart_count,
5664
+ request: observation.request,
5665
+ response: observation.response,
5666
+ extraction: observation.extraction,
5667
+ schema_validated: observation.schema_validated
5668
+ };
5669
+ if (pages.length >= 16 && page.response) {
5670
+ page.response = {
5671
+ ...page.response,
5672
+ payload: {
5673
+ response_not_retained: "Physical page was checked; response evidence exceeded the 16-page retention budget."
5674
+ }
5675
+ };
5676
+ }
5677
+ pages.push(page);
5678
+ }
5679
+ });
5680
+ if (options.signal?.aborted) options.signal.throwIfAborted();
5681
+ const logical = collection.ok ? {
5682
+ ...collection.catalog.firstPage.result.data,
5683
+ products: collection.catalog.products,
5684
+ ...collection.catalog.errors.length > 0 && { errors: collection.catalog.errors }
5685
+ } : void 0;
5686
+ if (logical) delete logical.pagination;
5687
+ productPageCollection = {
5688
+ complete: collection.ok,
5689
+ page_count: pages.length,
5690
+ restart_count: pages.reduce((maximum, page) => Math.max(maximum, page.restart_count), 0),
5691
+ pages,
5692
+ ...logical && { logical_response: redactSecrets(logical) }
5693
+ };
5694
+ collectedRequestRecord = collection.ok ? collection.catalog.firstPage.request : collection.exchange?.request;
5695
+ const observedResponse = collection.ok ? collection.catalog.firstPage.response : collection.exchange?.response;
5696
+ responseRecord = observedResponse ?? void 0;
5697
+ actualCollectedResponse = collection.ok ? collection.catalog.firstPage.result.data : collection.exchange?.response?.payload;
5698
+ taskResult = collection.ok ? { ...collection.catalog.firstPage.result, data: logical } : {
5699
+ success: false,
5700
+ error: collection.error.replace(/^fixture discovery/, "Product page collection")
5701
+ };
5702
+ stepResult = {
5703
+ duration_ms: Date.now() - started,
5704
+ passed: collection.ok && pagesPassed,
5705
+ ...!collection.ok && { error: collection.error.replace(/^fixture discovery/, "Product page collection") }
5706
+ };
5707
+ collectedPageValidations = pageValidations;
5708
+ if (!collection.ok && !pageValidations.some((validation) => validationFailsStep(validation))) {
5709
+ collectedPageValidations.push({
5710
+ check: "product_page_collection",
5711
+ passed: false,
5712
+ description: "Wholesale collection completes one stable, duplicate-free feed",
5713
+ error: collection.error,
5714
+ json_pointer: null,
5715
+ expected: "a complete stable wholesale collection",
5716
+ actual: collection.error,
5717
+ schema_id: null,
5718
+ schema_url: null,
5719
+ ...collection.exchange?.request && { request: collection.exchange.request },
5720
+ response: collection.exchange?.response ?? null
5721
+ });
5722
+ }
5723
+ } else if (step.parallel_dispatch) {
5541
5724
  const started = Date.now();
5542
5725
  crossResponses = await runParallelDispatches(stepClient(), effectiveStep.task, request, {
5543
5726
  spec: step.parallel_dispatch,
@@ -5625,7 +5808,7 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
5625
5808
  const requestRecord = {
5626
5809
  transport: options.protocol === "a2a" ? "a2a" : "mcp",
5627
5810
  operation: effectiveStep.task,
5628
- payload: redactSecrets(request),
5811
+ payload: collectedRequestRecord?.payload ?? redactSecrets(request),
5629
5812
  ...effectiveRequestUrl ? { url: redactOAuthUrlForOutput(effectiveRequestUrl) } : {}
5630
5813
  };
5631
5814
  const inputSchemaStripNotices = collectInputSchemaFieldStripNotices(
@@ -5813,9 +5996,9 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
5813
5996
  success: true,
5814
5997
  data: schemaValidationError.data
5815
5998
  } : void 0);
5816
- let validations = [];
5999
+ let validations = [...collectedPageValidations];
5817
6000
  if (step.validations?.length && (validationTaskResult || httpResult)) {
5818
- const resolvedValidations = step.validations.map((v) => {
6001
+ const resolvedValidations = step.validations.filter((v) => !productPageCollection || v.check !== "response_schema").map((v) => {
5819
6002
  const resolved = { ...v };
5820
6003
  if (resolved.value !== void 0) {
5821
6004
  resolved.value = injectContext({ __v: resolved.value }, context, runState.runnerVars).__v;
@@ -5872,9 +6055,11 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
5872
6055
  };
5873
6056
  })()
5874
6057
  };
5875
- validations = runValidations(
5876
- resolvedValidations.filter((validation) => validation.check !== "property_list_reference"),
5877
- vctx
6058
+ validations.push(
6059
+ ...runValidations(
6060
+ resolvedValidations.filter((validation) => validation.check !== "property_list_reference"),
6061
+ vctx
6062
+ )
5878
6063
  );
5879
6064
  for (const validation of resolvedValidations.filter((validation2) => validation2.check === "property_list_reference")) {
5880
6065
  const result = effectiveStep.task !== "create_property_list" || !validationTaskResult?.success ? {
@@ -6144,6 +6329,14 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
6144
6329
  }
6145
6330
  }
6146
6331
  }
6332
+ if (productPageCollection) {
6333
+ for (const validation of validations) {
6334
+ if (collectedPageValidations.includes(validation)) continue;
6335
+ if (validation.json_pointer)
6336
+ validation.json_pointer = `/product_page_collection/logical_response${validation.json_pointer}`;
6337
+ validation.description = `[logical product collection] ${validation.description ?? validation.check}`;
6338
+ }
6339
+ }
6147
6340
  if (protectedViolations.length > 0) validations.push(...protectedViolations);
6148
6341
  const allValidationsPassedFinal = validations.every((v) => !validationFailsStep(v));
6149
6342
  const stepFailed = !(passed && allValidationsPassedFinal);
@@ -6173,7 +6366,8 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
6173
6366
  // Legacy `response` field (new code reads `response_record`).
6174
6367
  // Redact in case a downstream consumer still keys off it; the
6175
6368
  // modern `response_record.payload` path is already redacted.
6176
- response: redactSecrets(taskResult?.data),
6369
+ response: redactSecrets(productPageCollection ? actualCollectedResponse : taskResult?.data),
6370
+ ...productPageCollection && { product_page_collection: productPageCollection },
6177
6371
  validations,
6178
6372
  // NOT projected here: the full runner threads this map from step to
6179
6373
  // step, so stripping the reserved namespace would break the very
@@ -6197,7 +6391,10 @@ async function executeStep(client, step, storyboardId, phaseId, context, allStep
6197
6391
  next,
6198
6392
  request: requestRecord,
6199
6393
  ...responseRecord && { response_record: responseRecord },
6200
- extraction: extractionFromTaskResult(taskResult),
6394
+ extraction: productPageCollection ? {
6395
+ path: "none",
6396
+ note: "Authored checks and context use product_page_collection.logical_response; response_record remains an actual physical page."
6397
+ } : extractionFromTaskResult(taskResult),
6201
6398
  ...inputSchemaStripNotices.length > 0 && { notices: inputSchemaStripNotices },
6202
6399
  ...hints.length > 0 && { hints }
6203
6400
  };
@@ -21,9 +21,11 @@
21
21
  * `controller_seeding_failed`.
22
22
  */
23
23
  import { type TestClient } from '../client.mjs';
24
+ import type { TaskResult } from '../types.mjs';
25
+ import type { DebugLogEntry } from '../../types/adcp.mjs';
24
26
  import { FixtureBindingRegistry } from './fixture-resolution.mjs';
25
27
  import { type CollectionSelectionWitness } from './collection-selection.mjs';
26
- import type { ContextProvenanceEntry, FixtureResolutionRecord, Storyboard, StoryboardContext, StoryboardFixtures, StoryboardPhaseResult, StoryboardRunOptions } from './types.mjs';
28
+ import type { ContextProvenanceEntry, RunnerExtractionRecord, RunnerRequestRecord, RunnerResponseRecord, FixtureResolutionRecord, Storyboard, StoryboardContext, StoryboardFixtures, StoryboardPhaseResult, StoryboardRunOptions } from './types.mjs';
27
29
  /** Synthetic phase id used in `StoryboardResult.phases[]` for the seed pass. */
28
30
  export declare const CONTROLLER_SEEDING_PHASE_ID = "__controller_seeding__";
29
31
  /** Seed scenario names. Kept local — the server-side `SEED_SCENARIOS`
@@ -101,4 +103,87 @@ export declare function runControllerSeeding(client: TestClient, storyboard: Sto
101
103
  client: TestClient;
102
104
  options: StoryboardRunOptions;
103
105
  }): Promise<ControllerSeedingResult | null>;
106
+ /**
107
+ * One actual `get_products` exchange from the discovery ladder.
108
+ *
109
+ * `runner-output-contract.yaml` makes `request` and `response` required on
110
+ * a failing validation, with null reserved for pre-wire failures. A capture
111
+ * that cannot be built from a pinned entity is NOT pre-wire — the entity
112
+ * came off the wire — so the failing result has to carry the exchange that
113
+ * produced it rather than an explicit "no contact" null.
114
+ */
115
+ export interface DiscoveryExchange {
116
+ request: RunnerRequestRecord;
117
+ response: RunnerResponseRecord;
118
+ }
119
+ export interface DiscoveryCatalog {
120
+ products: Array<Record<string, unknown>>;
121
+ firstPage: ProductPageObservation;
122
+ errors: unknown[];
123
+ evidence: {
124
+ page_count: number;
125
+ product_count: number;
126
+ cursors: string[];
127
+ restart_count?: number;
128
+ };
129
+ /** Per-page exchanges, in request order. */
130
+ exchanges: DiscoveryExchange[];
131
+ /** product_id → index into `exchanges`, so a failure cites ITS page. */
132
+ pageOfProduct: Map<string, number>;
133
+ }
134
+ export type DiscoveryCatalogResult = {
135
+ ok: true;
136
+ catalog: DiscoveryCatalog;
137
+ } | {
138
+ ok: false;
139
+ error: string;
140
+ evidence?: unknown;
141
+ /**
142
+ * The actual exchange that failed, so the producer step can record
143
+ * truthful wire evidence instead of nothing. `response` is null only
144
+ * when the transport threw and no response was ever received — never
145
+ * a stand-in for one we did not capture.
146
+ */
147
+ exchange?: {
148
+ request: RunnerRequestRecord;
149
+ response: RunnerResponseRecord | null;
150
+ extraction?: RunnerExtractionRecord;
151
+ };
152
+ /** AJV detail when the page parsed but failed its response schema. */
153
+ unavailable?: true;
154
+ schemaFailure?: {
155
+ detail: string;
156
+ schema_id: string;
157
+ schema_url: string;
158
+ };
159
+ };
160
+ /** One actual page, observed before any restart can discard its products. */
161
+ type ProductPageTaskResult = TaskResult & {
162
+ debug_logs?: DebugLogEntry[];
163
+ };
164
+ export interface ProductPageObservation {
165
+ page_index: number;
166
+ restart_count: number;
167
+ request: RunnerRequestRecord;
168
+ response: RunnerResponseRecord | null;
169
+ extraction: RunnerExtractionRecord;
170
+ result?: ProductPageTaskResult;
171
+ schema_validated: boolean;
172
+ /** A cached JSON-schema validator, rather than the legacy fallback, checked this page. */
173
+ strict_schema_validated?: boolean;
174
+ schemaFailure?: {
175
+ detail: string;
176
+ schema_id: string;
177
+ schema_url: string;
178
+ };
179
+ }
180
+ export interface ProductPageCollectionControls {
181
+ /** Preserve the normal step's execution policy and actual authored arguments. */
182
+ dispatch?: (request: Record<string, unknown>) => Promise<ProductPageTaskResult>;
183
+ /** Runs for every observed exchange, including pages later abandoned on restart. */
184
+ onPage?: (page: ProductPageObservation) => Promise<void>;
185
+ retainRestartEvidence?: boolean;
186
+ }
187
+ /** Shared strict unkeyed wholesale walker; callers own the initial query and logical view. */
188
+ export declare function collectWholesaleProductPages(client: TestClient, options: StoryboardRunOptions, query: Record<string, unknown>, controls?: ProductPageCollectionControls): Promise<DiscoveryCatalogResult>;
104
189
  export {};
@@ -21,9 +21,11 @@
21
21
  * `controller_seeding_failed`.
22
22
  */
23
23
  import { type TestClient } from '../client';
24
+ import type { TaskResult } from '../types';
25
+ import type { DebugLogEntry } from '../../types/adcp';
24
26
  import { FixtureBindingRegistry } from './fixture-resolution';
25
27
  import { type CollectionSelectionWitness } from './collection-selection';
26
- import type { ContextProvenanceEntry, FixtureResolutionRecord, Storyboard, StoryboardContext, StoryboardFixtures, StoryboardPhaseResult, StoryboardRunOptions } from './types';
28
+ import type { ContextProvenanceEntry, RunnerExtractionRecord, RunnerRequestRecord, RunnerResponseRecord, FixtureResolutionRecord, Storyboard, StoryboardContext, StoryboardFixtures, StoryboardPhaseResult, StoryboardRunOptions } from './types';
27
29
  /** Synthetic phase id used in `StoryboardResult.phases[]` for the seed pass. */
28
30
  export declare const CONTROLLER_SEEDING_PHASE_ID = "__controller_seeding__";
29
31
  /** Seed scenario names. Kept local — the server-side `SEED_SCENARIOS`
@@ -101,4 +103,87 @@ export declare function runControllerSeeding(client: TestClient, storyboard: Sto
101
103
  client: TestClient;
102
104
  options: StoryboardRunOptions;
103
105
  }): Promise<ControllerSeedingResult | null>;
106
+ /**
107
+ * One actual `get_products` exchange from the discovery ladder.
108
+ *
109
+ * `runner-output-contract.yaml` makes `request` and `response` required on
110
+ * a failing validation, with null reserved for pre-wire failures. A capture
111
+ * that cannot be built from a pinned entity is NOT pre-wire — the entity
112
+ * came off the wire — so the failing result has to carry the exchange that
113
+ * produced it rather than an explicit "no contact" null.
114
+ */
115
+ export interface DiscoveryExchange {
116
+ request: RunnerRequestRecord;
117
+ response: RunnerResponseRecord;
118
+ }
119
+ export interface DiscoveryCatalog {
120
+ products: Array<Record<string, unknown>>;
121
+ firstPage: ProductPageObservation;
122
+ errors: unknown[];
123
+ evidence: {
124
+ page_count: number;
125
+ product_count: number;
126
+ cursors: string[];
127
+ restart_count?: number;
128
+ };
129
+ /** Per-page exchanges, in request order. */
130
+ exchanges: DiscoveryExchange[];
131
+ /** product_id → index into `exchanges`, so a failure cites ITS page. */
132
+ pageOfProduct: Map<string, number>;
133
+ }
134
+ export type DiscoveryCatalogResult = {
135
+ ok: true;
136
+ catalog: DiscoveryCatalog;
137
+ } | {
138
+ ok: false;
139
+ error: string;
140
+ evidence?: unknown;
141
+ /**
142
+ * The actual exchange that failed, so the producer step can record
143
+ * truthful wire evidence instead of nothing. `response` is null only
144
+ * when the transport threw and no response was ever received — never
145
+ * a stand-in for one we did not capture.
146
+ */
147
+ exchange?: {
148
+ request: RunnerRequestRecord;
149
+ response: RunnerResponseRecord | null;
150
+ extraction?: RunnerExtractionRecord;
151
+ };
152
+ /** AJV detail when the page parsed but failed its response schema. */
153
+ unavailable?: true;
154
+ schemaFailure?: {
155
+ detail: string;
156
+ schema_id: string;
157
+ schema_url: string;
158
+ };
159
+ };
160
+ /** One actual page, observed before any restart can discard its products. */
161
+ type ProductPageTaskResult = TaskResult & {
162
+ debug_logs?: DebugLogEntry[];
163
+ };
164
+ export interface ProductPageObservation {
165
+ page_index: number;
166
+ restart_count: number;
167
+ request: RunnerRequestRecord;
168
+ response: RunnerResponseRecord | null;
169
+ extraction: RunnerExtractionRecord;
170
+ result?: ProductPageTaskResult;
171
+ schema_validated: boolean;
172
+ /** A cached JSON-schema validator, rather than the legacy fallback, checked this page. */
173
+ strict_schema_validated?: boolean;
174
+ schemaFailure?: {
175
+ detail: string;
176
+ schema_id: string;
177
+ schema_url: string;
178
+ };
179
+ }
180
+ export interface ProductPageCollectionControls {
181
+ /** Preserve the normal step's execution policy and actual authored arguments. */
182
+ dispatch?: (request: Record<string, unknown>) => Promise<ProductPageTaskResult>;
183
+ /** Runs for every observed exchange, including pages later abandoned on restart. */
184
+ onPage?: (page: ProductPageObservation) => Promise<void>;
185
+ retainRestartEvidence?: boolean;
186
+ }
187
+ /** Shared strict unkeyed wholesale walker; callers own the initial query and logical view. */
188
+ export declare function collectWholesaleProductPages(client: TestClient, options: StoryboardRunOptions, query: Record<string, unknown>, controls?: ProductPageCollectionControls): Promise<DiscoveryCatalogResult>;
104
189
  export {};