@adcp/sdk 16.1.0 → 16.3.0

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 (126) hide show
  1. package/AGENTS.md +2 -0
  2. package/dist/lib/core/AgentClient.js +17 -1
  3. package/dist/lib/core/AgentClient.mjs +17 -1
  4. package/dist/lib/core/SingleAgentClient.js +13 -0
  5. package/dist/lib/core/SingleAgentClient.mjs +13 -0
  6. package/dist/lib/core/TaskExecutor.js +88 -13
  7. package/dist/lib/core/TaskExecutor.mjs +89 -14
  8. package/dist/lib/errors/index.d.mts +34 -0
  9. package/dist/lib/errors/index.d.ts +34 -0
  10. package/dist/lib/errors/index.js +11 -0
  11. package/dist/lib/errors/index.mjs +10 -0
  12. package/dist/lib/governance/authorization.d.mts +66 -0
  13. package/dist/lib/governance/authorization.d.ts +66 -0
  14. package/dist/lib/governance/authorization.js +181 -37
  15. package/dist/lib/governance/authorization.mjs +183 -36
  16. package/dist/lib/governance/index.d.mts +2 -2
  17. package/dist/lib/governance/index.d.ts +2 -2
  18. package/dist/lib/governance/index.js +4 -2
  19. package/dist/lib/governance/index.mjs +3 -1
  20. package/dist/lib/identity-3.3/index.d.mts +1 -0
  21. package/dist/lib/identity-3.3/index.d.ts +1 -0
  22. package/dist/lib/identity-3.3/index.js +3 -1
  23. package/dist/lib/identity-3.3/index.mjs +1 -0
  24. package/dist/lib/identity-3.3/jws-artifacts.d.mts +147 -0
  25. package/dist/lib/identity-3.3/jws-artifacts.d.ts +147 -0
  26. package/dist/lib/identity-3.3/jws-artifacts.js +258 -0
  27. package/dist/lib/identity-3.3/jws-artifacts.mjs +234 -0
  28. package/dist/lib/identity-3.3/revocation.d.mts +7 -0
  29. package/dist/lib/identity-3.3/revocation.d.ts +7 -0
  30. package/dist/lib/identity-3.3/revocation.js +13 -0
  31. package/dist/lib/identity-3.3/revocation.mjs +13 -0
  32. package/dist/lib/index.d.mts +3 -3
  33. package/dist/lib/index.d.ts +3 -3
  34. package/dist/lib/index.js +4 -0
  35. package/dist/lib/index.mjs +4 -0
  36. package/dist/lib/media-buy/compatibility.d.mts +1 -0
  37. package/dist/lib/media-buy/compatibility.d.ts +1 -0
  38. package/dist/lib/media-buy/compatibility.js +33 -10
  39. package/dist/lib/media-buy/compatibility.mjs +32 -14
  40. package/dist/lib/negotiation/buyer.js +11 -5
  41. package/dist/lib/negotiation/buyer.mjs +11 -5
  42. package/dist/lib/negotiation/types.d.mts +4 -2
  43. package/dist/lib/negotiation/types.d.ts +4 -2
  44. package/dist/lib/negotiation/verification.d.mts +15 -6
  45. package/dist/lib/negotiation/verification.d.ts +15 -6
  46. package/dist/lib/negotiation/verification.js +146 -27
  47. package/dist/lib/negotiation/verification.mjs +146 -28
  48. package/dist/lib/negotiation/version.d.mts +6 -0
  49. package/dist/lib/negotiation/version.d.ts +6 -0
  50. package/dist/lib/negotiation/version.js +60 -0
  51. package/dist/lib/negotiation/version.mjs +33 -0
  52. package/dist/lib/registry/types.generated.d.mts +24 -1
  53. package/dist/lib/registry/types.generated.d.ts +24 -1
  54. package/dist/lib/server/create-adcp-server.js +1 -1
  55. package/dist/lib/server/create-adcp-server.mjs +1 -1
  56. package/dist/lib/server/error-arm-tools.d.mts +10 -10
  57. package/dist/lib/server/error-arm-tools.d.ts +10 -10
  58. package/dist/lib/server/error-arm-tools.js +22 -58
  59. package/dist/lib/server/error-arm-tools.mjs +30 -65
  60. package/dist/lib/server/index.d.mts +2 -2
  61. package/dist/lib/server/index.d.ts +2 -2
  62. package/dist/lib/server/index.js +2 -0
  63. package/dist/lib/server/index.mjs +2 -0
  64. package/dist/lib/signing/web-bot-auth.js +5 -0
  65. package/dist/lib/signing/web-bot-auth.mjs +4 -0
  66. package/dist/lib/testing/compliance/comply.js +17 -0
  67. package/dist/lib/testing/compliance/comply.mjs +17 -0
  68. package/dist/lib/testing/index.d.mts +3 -0
  69. package/dist/lib/testing/index.d.ts +3 -0
  70. package/dist/lib/testing/index.js +44 -2
  71. package/dist/lib/testing/index.mjs +42 -0
  72. package/dist/lib/testing/storyboard/collection-selection.d.mts +259 -0
  73. package/dist/lib/testing/storyboard/collection-selection.d.ts +259 -0
  74. package/dist/lib/testing/storyboard/collection-selection.js +384 -0
  75. package/dist/lib/testing/storyboard/collection-selection.mjs +348 -0
  76. package/dist/lib/testing/storyboard/context.js +12 -6
  77. package/dist/lib/testing/storyboard/context.mjs +12 -6
  78. package/dist/lib/testing/storyboard/fixture-resolution.d.mts +39 -1
  79. package/dist/lib/testing/storyboard/fixture-resolution.d.ts +39 -1
  80. package/dist/lib/testing/storyboard/fixture-resolution.js +216 -12
  81. package/dist/lib/testing/storyboard/fixture-resolution.mjs +218 -12
  82. package/dist/lib/testing/storyboard/index.d.mts +9 -2
  83. package/dist/lib/testing/storyboard/index.d.ts +9 -2
  84. package/dist/lib/testing/storyboard/index.js +51 -2
  85. package/dist/lib/testing/storyboard/index.mjs +49 -1
  86. package/dist/lib/testing/storyboard/prepared-fixtures.d.mts +471 -0
  87. package/dist/lib/testing/storyboard/prepared-fixtures.d.ts +471 -0
  88. package/dist/lib/testing/storyboard/prepared-fixtures.js +434 -0
  89. package/dist/lib/testing/storyboard/prepared-fixtures.mjs +389 -0
  90. package/dist/lib/testing/storyboard/protected-context.d.mts +127 -0
  91. package/dist/lib/testing/storyboard/protected-context.d.ts +127 -0
  92. package/dist/lib/testing/storyboard/protected-context.js +301 -0
  93. package/dist/lib/testing/storyboard/protected-context.mjs +267 -0
  94. package/dist/lib/testing/storyboard/rejection-hints.js +13 -4
  95. package/dist/lib/testing/storyboard/rejection-hints.mjs +13 -4
  96. package/dist/lib/testing/storyboard/runner.d.mts +24 -0
  97. package/dist/lib/testing/storyboard/runner.d.ts +24 -0
  98. package/dist/lib/testing/storyboard/runner.js +1061 -105
  99. package/dist/lib/testing/storyboard/runner.mjs +1075 -106
  100. package/dist/lib/testing/storyboard/seeding.d.mts +15 -1
  101. package/dist/lib/testing/storyboard/seeding.d.ts +15 -1
  102. package/dist/lib/testing/storyboard/seeding.js +272 -12
  103. package/dist/lib/testing/storyboard/seeding.mjs +274 -13
  104. package/dist/lib/testing/storyboard/types.d.mts +207 -10
  105. package/dist/lib/testing/storyboard/types.d.ts +207 -10
  106. package/dist/lib/testing/storyboard/types.js +1 -1
  107. package/dist/lib/testing/storyboard/types.mjs +1 -1
  108. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  109. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  110. package/dist/lib/testing/storyboard/validations.js +95 -0
  111. package/dist/lib/testing/storyboard/validations.mjs +95 -0
  112. package/dist/lib/validation/bundled-schema-store.js +5 -0
  113. package/dist/lib/validation/bundled-schema-store.mjs +4 -0
  114. package/dist/lib/validation/schema-loader.js +19 -2
  115. package/dist/lib/validation/schema-loader.mjs +22 -3
  116. package/dist/lib/version.d.mts +3 -3
  117. package/dist/lib/version.d.ts +3 -3
  118. package/dist/lib/version.js +3 -3
  119. package/dist/lib/version.mjs +3 -3
  120. package/docs/TYPE-SUMMARY.md +2 -2
  121. package/docs/guides/COLLECTION-FIXTURE-FOUNDATION.md +850 -0
  122. package/docs/guides/PROPOSAL-TERMS-VERIFICATION.md +66 -0
  123. package/docs/llms.txt +4 -3
  124. package/docs/migration-14.x-rc-worksheet.md +4 -4
  125. package/docs/migration-agent-identity-3.3.md +51 -6
  126. package/package.json +6 -4
@@ -8,8 +8,12 @@ import {
8
8
  buildFixtureResolutionSpecs,
9
9
  applyFixtureBindingsToRequest,
10
10
  FixtureBindingRegistry,
11
- matchesFixtureRequirements
11
+ matchesFixtureRequirements,
12
+ matchFixtureRequirementsWithWitness
12
13
  } from "./fixture-resolution.mjs";
14
+ import { readCollectionWitness } from "./collection-selection.mjs";
15
+ import { resolvePath } from "./path.mjs";
16
+ import { redactSecrets } from "../../utils/redact-secrets.mjs";
13
17
  const CONTROLLER_SEEDING_PHASE_ID = "__controller_seeding__";
14
18
  function buildSeedCalls(fixtures) {
15
19
  if (!fixtures) return [];
@@ -359,6 +363,10 @@ async function runRoutedControllerSeeding(storyboard, calls, context, resolveFix
359
363
  failedCount
360
364
  };
361
365
  }
366
+ function discoveryExchangeFor(catalog, productId) {
367
+ const index = productId === void 0 ? void 0 : catalog.pageOfProduct.get(productId);
368
+ return catalog.exchanges[index ?? catalog.exchanges.length - 1];
369
+ }
362
370
  async function runDeclaredFixtureResolution(seedClient, discoveryClient, storyboard, options, context, calls, resolveFixtureAgent) {
363
371
  const start = Date.now();
364
372
  const authoringErrorResult = buildAuthoringErrorResult(storyboard, calls, context, start);
@@ -387,6 +395,11 @@ async function runDeclaredFixtureResolution(seedClient, discoveryClient, storybo
387
395
  const claimedProducts = /* @__PURE__ */ new Map();
388
396
  const claimedPricingOptions = /* @__PURE__ */ new Map();
389
397
  const productStatuses = /* @__PURE__ */ new Map();
398
+ const pendingCaptures = [];
399
+ const retainedWitnesses = /* @__PURE__ */ new Map();
400
+ let captureFailure;
401
+ const captureFailures = /* @__PURE__ */ new Map();
402
+ const captureFailedHandles = /* @__PURE__ */ new Set();
390
403
  const seedContext = { correlation_id: `${storyboardCorrelationPrefix(storyboard)}--__seeding__` };
391
404
  let seedOptions = options;
392
405
  let discoveryOptions = options;
@@ -519,6 +532,43 @@ async function runDeclaredFixtureResolution(seedClient, discoveryClient, storybo
519
532
  const data = raw.data;
520
533
  if (raw.success && data?.success === true) {
521
534
  boundIds = bindSeededFixture(spec, bindings);
535
+ const seedSnapshot = {
536
+ ...spec.fixture,
537
+ ...spec.entityType === "product" ? { product_id: boundIds.product_id } : { pricing_option_id: boundIds.pricing_option_id }
538
+ };
539
+ const seedWitness = matchFixtureRequirementsWithWitness(seedSnapshot, spec.clauses).witness;
540
+ if (seedWitness) retainedWitnesses.set(spec.handle, seedWitness);
541
+ const seedCaptures = collectDeclaredCaptures(spec, seedSnapshot, seedWitness);
542
+ if (!seedCaptures.ok) {
543
+ failure = seedCaptures.detail;
544
+ captureFailure ??= seedCaptures.detail;
545
+ captureFailures.set(spec.handle, {
546
+ ...seedCaptures.path !== void 0 && { pointer: toJsonPointer(seedCaptures.path) },
547
+ exchange: {
548
+ request: {
549
+ transport: "mcp",
550
+ operation: "comply_test_controller",
551
+ // The object actually handed to `callControllerRaw`,
552
+ // not `controllerRequest` — the call substitutes
553
+ // `call.params` when the bound params are not a plain
554
+ // record, and adds the seeding correlation context.
555
+ payload: redactSecrets({
556
+ scenario: call.scenario,
557
+ params: isPlainRecord(controllerRequest.params) ? controllerRequest.params : call.params,
558
+ context: seedContext
559
+ })
560
+ },
561
+ response: { transport: "mcp", payload: redactSecrets(raw.data ?? raw.error ?? null) }
562
+ }
563
+ });
564
+ captureFailedHandles.add(spec.handle);
565
+ attempts.push({ strategy, disposition: "failed", detail: seedCaptures.detail });
566
+ boundIds = void 0;
567
+ break;
568
+ }
569
+ pendingCaptures.push(
570
+ ...seedCaptures.captures.map((c) => ({ ...c, sourceTask: "comply_test_controller" }))
571
+ );
522
572
  if (spec.entityType === "product") {
523
573
  recordIdentityClaim(claimedProducts, boundIds.product_id, spec.allowReuse);
524
574
  } else {
@@ -574,7 +624,10 @@ async function runDeclaredFixtureResolution(seedClient, discoveryClient, storybo
574
624
  discovered.catalog.products,
575
625
  bindings,
576
626
  claimedProducts,
577
- claimedPricingOptions
627
+ claimedPricingOptions,
628
+ spec.entityType === "product" ? specs.filter(
629
+ (candidate) => candidate.entityType === "product_pricing_option" && candidate.parentProductHandle === spec.handle
630
+ ) : []
578
631
  );
579
632
  if (!selected.ok) {
580
633
  if (selected.failed) {
@@ -597,6 +650,27 @@ async function runDeclaredFixtureResolution(seedClient, discoveryClient, storybo
597
650
  }
598
651
  boundIds = selected.boundIds;
599
652
  chosenStrategy = strategy;
653
+ if (selected.witness) retainedWitnesses.set(spec.handle, selected.witness);
654
+ const collected = collectDeclaredCaptures(spec, selected.snapshot, selected.witness);
655
+ if (!collected.ok) {
656
+ failure = collected.detail;
657
+ captureFailure ??= collected.detail;
658
+ const failureExchange = discoveryExchangeFor(discovered.catalog, selected.boundIds.product_id);
659
+ captureFailures.set(spec.handle, {
660
+ ...collected.path !== void 0 && { pointer: toJsonPointer(collected.path) },
661
+ ...failureExchange && { exchange: failureExchange }
662
+ });
663
+ captureFailedHandles.add(spec.handle);
664
+ attempts.push({
665
+ strategy,
666
+ disposition: "failed",
667
+ detail: collected.detail,
668
+ response: { ...discovered.catalog.evidence, selected: selected.boundIds }
669
+ });
670
+ boundIds = void 0;
671
+ break;
672
+ }
673
+ pendingCaptures.push(...collected.captures.map((c) => ({ ...c, sourceTask: "get_products" })));
600
674
  attempts.push({
601
675
  strategy,
602
676
  disposition: "resolved",
@@ -620,6 +694,7 @@ async function runDeclaredFixtureResolution(seedClient, discoveryClient, storybo
620
694
  };
621
695
  records.push(record);
622
696
  if (status !== "unsatisfied") {
697
+ const captureFailed = status === "failed" && captureFailedHandles.has(spec.handle);
623
698
  steps.push({
624
699
  storyboard_id: storyboard.id,
625
700
  step_id: call.step_id,
@@ -628,7 +703,29 @@ async function runDeclaredFixtureResolution(seedClient, discoveryClient, storybo
628
703
  task: chosenStrategy === "discover" ? "get_products" : "comply_test_controller",
629
704
  passed: status !== "failed",
630
705
  duration_ms: Date.now() - stepStart,
631
- validations: [],
706
+ // An unbuildable capture gets a `check` discriminator like the other
707
+ // two failure classes, so a report renderer keys on `check` for all
708
+ // three instead of regexing prose for this one.
709
+ validations: captureFailed ? [
710
+ {
711
+ check: "fixture_capture_unbuildable",
712
+ passed: false,
713
+ description: "Every declared fixture capture must be buildable from the pinned entity",
714
+ error: failure,
715
+ // The contract's analogue, `capture_path_not_resolvable`,
716
+ // requires the RFC 6901 form of the `context_outputs` path
717
+ // that failed. `null` only for the witness and size/depth
718
+ // arms, which genuinely have no path.
719
+ json_pointer: captureFailures.get(spec.handle)?.pointer ?? null,
720
+ expected: "a value at the declared selected-entity path, or a derivable witness",
721
+ actual: null,
722
+ // required_on_failure: null only when the check is not a
723
+ // response_schema check, which this never is.
724
+ schema_id: null,
725
+ schema_url: null,
726
+ ...captureFailures.get(spec.handle)?.exchange ?? {}
727
+ }
728
+ ] : [],
632
729
  context,
633
730
  extraction: { path: "none" },
634
731
  ...failure && { error: failure }
@@ -639,6 +736,23 @@ async function runDeclaredFixtureResolution(seedClient, discoveryClient, storybo
639
736
  else passedCount++;
640
737
  }
641
738
  const allPassed = failedCount === 0;
739
+ const graphComplete = allPassed && !fixtureUnsatisfied && captureFailure === void 0 && records.every((r) => r.status === "resolved");
740
+ const preparedCaptures = graphComplete && pendingCaptures.length > 0 ? {
741
+ values: Object.fromEntries(pendingCaptures.map((entry) => [entry.key, entry.value])),
742
+ provenance: Object.fromEntries(
743
+ pendingCaptures.map((entry) => [
744
+ entry.key,
745
+ {
746
+ source_step_id: CONTROLLER_SEEDING_PHASE_ID,
747
+ source_kind: "fixture_preparation",
748
+ source_task: entry.sourceTask,
749
+ fixture_handle: entry.spec.handle,
750
+ ...entry.spec.parentProductHandle && { fixture_product_handle: entry.spec.parentProductHandle }
751
+ }
752
+ ])
753
+ ),
754
+ witnesses: Object.fromEntries(retainedWitnesses)
755
+ } : void 0;
642
756
  return {
643
757
  phase: {
644
758
  phase_id: CONTROLLER_SEEDING_PHASE_ID,
@@ -652,8 +766,76 @@ async function runDeclaredFixtureResolution(seedClient, discoveryClient, storybo
652
766
  failedCount,
653
767
  fixtureUnsatisfied,
654
768
  bindings,
655
- resolutionRecords: records
769
+ resolutionRecords: records,
770
+ ...preparedCaptures && { preparedCaptures }
771
+ };
772
+ }
773
+ function toJsonPointer(path) {
774
+ if (path === void 0 || path.length === 0) return void 0;
775
+ const segments = path.replace(/\[(\d+)\]/g, ".$1").split(".").filter((segment) => segment.length > 0).map((segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1"));
776
+ return segments.length > 0 ? `/${segments.join("/")}` : void 0;
777
+ }
778
+ const DISCOVERY_RETAINED_RESPONSE_PAGES = 16;
779
+ const CAPTURE_MAX_DEPTH = 64;
780
+ const CAPTURE_MAX_BYTES = 512 * 1024;
781
+ function describeUncapturableValue(value, key) {
782
+ let depth = 0;
783
+ const walk = (node, level) => {
784
+ if (level > depth) depth = level;
785
+ if (depth > CAPTURE_MAX_DEPTH) return false;
786
+ if (node === null || typeof node !== "object") return true;
787
+ if (Array.isArray(node)) return node.every((item) => walk(item, level + 1));
788
+ return Object.values(node).every((item) => walk(item, level + 1));
656
789
  };
790
+ if (!walk(value, 1)) {
791
+ return `fixture capture "${key}" nests deeper than the ${CAPTURE_MAX_DEPTH}-level capture limit`;
792
+ }
793
+ let size;
794
+ try {
795
+ size = Buffer.byteLength(JSON.stringify(value) ?? "", "utf8");
796
+ } catch {
797
+ return `fixture capture "${key}" is not JSON-serializable and cannot be detached`;
798
+ }
799
+ if (size > CAPTURE_MAX_BYTES) {
800
+ return `fixture capture "${key}" serializes to ${size} bytes, over the ${CAPTURE_MAX_BYTES}-byte capture limit`;
801
+ }
802
+ return void 0;
803
+ }
804
+ function collectDeclaredCaptures(spec, snapshot, witness) {
805
+ if (spec.contextOutputs.length === 0) return { ok: true, captures: [] };
806
+ const captures = [];
807
+ for (const output of spec.contextOutputs) {
808
+ if (output.snapshot !== void 0) {
809
+ captures.push({ spec, key: output.key, value: redactSecrets(snapshot) });
810
+ continue;
811
+ }
812
+ if (output.witness !== void 0) {
813
+ const read = readCollectionWitness(witness, output.witness);
814
+ if (!read.ok) {
815
+ return {
816
+ ok: false,
817
+ detail: `fixture capture "${output.key}" for handle "${spec.handle}" could not be built: ${read.detail}`
818
+ };
819
+ }
820
+ captures.push({ spec, key: output.key, value: read.value });
821
+ continue;
822
+ }
823
+ const value = resolvePath(redactSecrets(snapshot), output.path);
824
+ if (value === void 0 || value === null || value === "") {
825
+ const pinned = spec.entityType === "product" ? `product "${String(snapshot.product_id ?? spec.handle)}"` : `pricing option "${String(snapshot.pricing_option_id ?? spec.handle)}" on product "${spec.parentProductHandle}"`;
826
+ return {
827
+ ok: false,
828
+ path: output.path,
829
+ detail: `fixture capture "${output.key}" (handle "${spec.handle}", pinned ${pinned}) found no usable value at selected-entity path "${output.path}" \u2014 resolved to ${value === void 0 ? "absent" : JSON.stringify(value)}. Either the path does not exist on this entity shape (top-level keys: ${Object.keys(snapshot).join(", ")}), or this seller does not populate it. If the field is OPTIONAL in the schema (fixed_price and min_spend_per_package both are), capture the whole entity with { key: "${output.key}", snapshot: selected_entity } so absence stays absence. Add a match predicate for "${output.path}" only when carrying that field is a real eligibility requirement \u2014 gating on it stops a conformant seller qualifying at all.`
830
+ };
831
+ }
832
+ captures.push({ spec, key: output.key, value });
833
+ }
834
+ for (const capture of captures) {
835
+ const unusable = describeUncapturableValue(capture.value, capture.key);
836
+ if (unusable) return { ok: false, detail: unusable };
837
+ }
838
+ return { ok: true, captures };
657
839
  }
658
840
  function resolutionSpecKey(spec) {
659
841
  return spec.entityType === "product" ? `seed_product\0${spec.handle}` : `seed_pricing_option\0${spec.parentProductHandle}\0${spec.handle}`;
@@ -759,6 +941,8 @@ async function discoverProductCatalog(client, options, context) {
759
941
  const seenCursors = /* @__PURE__ */ new Set();
760
942
  let cursor;
761
943
  let pageCount = 0;
944
+ const exchanges = [];
945
+ const pageOfProduct = /* @__PURE__ */ new Map();
762
946
  do {
763
947
  if (pageCount >= 1e3) {
764
948
  return { ok: false, error: "fixture discovery pagination exceeded the 1,000-page safety limit" };
@@ -772,6 +956,7 @@ async function discoverProductCatalog(client, options, context) {
772
956
  ext: { adcp: { creative_wire: "canonical" } }
773
957
  };
774
958
  let result;
959
+ const pageStart = Date.now();
775
960
  try {
776
961
  result = await executeStoryboardTask(client, "get_products", request, { signal: options.signal });
777
962
  } catch (err) {
@@ -781,6 +966,19 @@ async function discoverProductCatalog(client, options, context) {
781
966
  };
782
967
  }
783
968
  pageCount++;
969
+ const retainResponse = exchanges.length < DISCOVERY_RETAINED_RESPONSE_PAGES;
970
+ const pageIndex = exchanges.push({
971
+ request: { transport: "mcp", operation: "get_products", payload: redactSecrets(request) },
972
+ response: {
973
+ transport: "mcp",
974
+ payload: retainResponse ? redactSecrets(
975
+ result.data ?? (result.adcp_error ? { adcp_error: result.adcp_error } : void 0) ?? result.error ?? null
976
+ ) : {
977
+ response_not_retained: `page ${exchanges.length + 1} is past the runner's ${DISCOVERY_RETAINED_RESPONSE_PAGES}-page discovery-evidence budget; the request for this page is recorded in full`
978
+ },
979
+ duration_ms: Date.now() - pageStart
980
+ }
981
+ }) - 1;
784
982
  if (!result.success) {
785
983
  return {
786
984
  ok: false,
@@ -842,6 +1040,7 @@ async function discoverProductCatalog(client, options, context) {
842
1040
  seenPricingIds.add(option.pricing_option_id);
843
1041
  }
844
1042
  }
1043
+ pageOfProduct.set(product.product_id, pageIndex);
845
1044
  products.push(product);
846
1045
  if (products.length > 1e5) {
847
1046
  return { ok: false, error: "fixture discovery exceeded the 100,000-product safety limit" };
@@ -865,7 +1064,12 @@ async function discoverProductCatalog(client, options, context) {
865
1064
  } while (cursor);
866
1065
  return {
867
1066
  ok: true,
868
- catalog: { products, evidence: { page_count: pageCount, product_count: products.length, cursors } }
1067
+ catalog: {
1068
+ products,
1069
+ evidence: { page_count: pageCount, product_count: products.length, cursors },
1070
+ exchanges,
1071
+ pageOfProduct
1072
+ }
869
1073
  };
870
1074
  }
871
1075
  function isPlainRecord(value) {
@@ -874,19 +1078,47 @@ function isPlainRecord(value) {
874
1078
  function utf8Compare(a, b) {
875
1079
  return Buffer.compare(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
876
1080
  }
877
- function selectDiscoveredFixture(spec, products, bindings, claimedProducts, claimedPricingOptions) {
1081
+ function selectDiscoveredFixture(spec, products, bindings, claimedProducts, claimedPricingOptions, dependentPricingSpecs = []) {
878
1082
  if (spec.entityType === "product") {
879
- const candidates2 = products.filter((product2) => matchesFixtureRequirements(product2, spec.clauses)).filter((product2) => identityCanBeClaimed(claimedProducts, product2.product_id, spec.allowReuse)).sort((a, b) => utf8Compare(a.product_id, b.product_id));
880
- const selected2 = candidates2[0];
881
- if (!selected2)
882
- return { ok: false, failed: false, detail: "no discovered product satisfied the authored requirements" };
1083
+ const rejections = /* @__PURE__ */ new Map();
1084
+ let claimedElsewhere = 0;
1085
+ let strandedChild = 0;
1086
+ const matched = [];
1087
+ for (const product2 of products) {
1088
+ const match = matchFixtureRequirementsWithWitness(product2, spec.clauses);
1089
+ if (!match.matched) {
1090
+ const reason = match.reason ?? "did not satisfy the authored match predicates";
1091
+ rejections.set(reason, (rejections.get(reason) ?? 0) + 1);
1092
+ continue;
1093
+ }
1094
+ if (!identityCanBeClaimed(claimedProducts, product2.product_id, spec.allowReuse)) {
1095
+ claimedElsewhere++;
1096
+ continue;
1097
+ }
1098
+ if (!dependentPricingSpecs.every((child) => productCanSatisfyPricingSpec(product2, child))) {
1099
+ strandedChild++;
1100
+ continue;
1101
+ }
1102
+ matched.push({ product: product2, ...match.witness && { witness: match.witness } });
1103
+ }
1104
+ matched.sort((a, b) => utf8Compare(a.product.product_id, b.product.product_id));
1105
+ const selected2 = matched[0]?.product;
1106
+ if (!selected2) {
1107
+ return {
1108
+ ok: false,
1109
+ failed: false,
1110
+ detail: describeProductNoMatch(spec, products.length, rejections, claimedElsewhere, strandedChild)
1111
+ };
1112
+ }
883
1113
  const productId = selected2.product_id;
884
1114
  bindings.bindProduct(spec.handle, productId);
885
1115
  recordIdentityClaim(claimedProducts, productId, spec.allowReuse);
886
1116
  return {
887
1117
  ok: true,
888
1118
  boundIds: { product_id: productId },
889
- detail: `selected product_id "${productId}" by UTF-8 bytewise order`
1119
+ detail: `selected product_id "${productId}" by UTF-8 bytewise order`,
1120
+ snapshot: selected2,
1121
+ ...matched[0].witness && { witness: matched[0].witness }
890
1122
  };
891
1123
  }
892
1124
  const parentHandle = spec.parentProductHandle;
@@ -921,7 +1153,7 @@ function selectDiscoveredFixture(spec, products, bindings, claimedProducts, clai
921
1153
  return {
922
1154
  ok: false,
923
1155
  failed: false,
924
- detail: "no pricing option on the bound product satisfied the authored requirements"
1156
+ detail: `no pricing option on bound product "${sellerProductId}" satisfied fixture handle "${parentHandle}/${spec.handle}" \u2014 ${options.length} option(s) on the pinned product` + (options.length > 0 ? `, pricing_option_id(s): ${options.filter(isPlainRecord).map((option) => String(option.pricing_option_id)).slice(0, 10).join(", ")}` : "")
925
1157
  };
926
1158
  }
927
1159
  const pricingOptionId = selected.pricing_option_id;
@@ -930,9 +1162,38 @@ function selectDiscoveredFixture(spec, products, bindings, claimedProducts, clai
930
1162
  return {
931
1163
  ok: true,
932
1164
  boundIds: { product_id: sellerProductId, pricing_option_id: pricingOptionId },
933
- detail: `selected pricing option "${sellerProductId}\0${pricingOptionId}" by UTF-8 bytewise order`
1165
+ detail: `selected pricing option "${sellerProductId}\0${pricingOptionId}" by UTF-8 bytewise order`,
1166
+ // The scoped price comes out of the same pinned product graph, so a
1167
+ // captured currency / fixed_price / min_spend_per_package describes the
1168
+ // option the buy will actually reference.
1169
+ snapshot: selected
934
1170
  };
935
1171
  }
1172
+ function describeProductNoMatch(spec, candidateCount, rejections, claimedElsewhere, strandedChild) {
1173
+ const parts = [
1174
+ `no discovered product satisfied fixture handle "${spec.handle}" \u2014 ${candidateCount} candidate(s) enumerated from the complete paginated catalog`
1175
+ ];
1176
+ const top = [...rejections].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([reason, count]) => `${count}x ${reason}`);
1177
+ if (top.length > 0) parts.push(`rejected by the authored match: ${top.join(" | ")}`);
1178
+ if (claimedElsewhere > 0) {
1179
+ parts.push(
1180
+ `${claimedElsewhere} otherwise-matching product(s) were already pinned to another handle \u2014 set allow_reuse: true on this declaration to share one seller entity`
1181
+ );
1182
+ }
1183
+ if (strandedChild > 0) {
1184
+ parts.push(
1185
+ `${strandedChild} matched but carried no pricing option the declared child pricing_options could bind \u2014 loosen the child match, or give the child a seed strategy`
1186
+ );
1187
+ }
1188
+ return parts.join(". ");
1189
+ }
1190
+ function productCanSatisfyPricingSpec(product, child) {
1191
+ if (child.strategies.length !== 1 || child.strategies[0] !== "discover") return true;
1192
+ const options = Array.isArray(product.pricing_options) ? product.pricing_options : [];
1193
+ return options.filter(isPlainRecord).some(
1194
+ (option) => typeof option.pricing_option_id === "string" && option.pricing_option_id.length > 0 && matchesFixtureRequirements(option, child.clauses)
1195
+ );
1196
+ }
936
1197
  function identityCanBeClaimed(claims, identity, allowReuse) {
937
1198
  const existingAllowsReuse = claims.get(identity);
938
1199
  return existingAllowsReuse === void 0 || existingAllowsReuse && allowReuse;