@adcp/sdk 16.2.0 → 16.4.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 (161) hide show
  1. package/AGENTS.md +2 -0
  2. package/dist/lib/adapters/version/3.1/assignments.d.mts +12 -0
  3. package/dist/lib/adapters/version/3.1/assignments.d.ts +12 -0
  4. package/dist/lib/adapters/version/3.1/assignments.js +67 -0
  5. package/dist/lib/adapters/version/3.1/assignments.mjs +43 -0
  6. package/dist/lib/adapters/version/3.1/bidding.d.mts +3 -0
  7. package/dist/lib/adapters/version/3.1/bidding.d.ts +3 -0
  8. package/dist/lib/adapters/version/3.1/bidding.js +115 -0
  9. package/dist/lib/adapters/version/3.1/bidding.mjs +90 -0
  10. package/dist/lib/adapters/version/3.1/delivery.d.mts +7 -0
  11. package/dist/lib/adapters/version/3.1/delivery.d.ts +7 -0
  12. package/dist/lib/adapters/version/3.1/delivery.js +78 -0
  13. package/dist/lib/adapters/version/3.1/delivery.mjs +54 -0
  14. package/dist/lib/adapters/version/3.1/index.d.mts +3 -0
  15. package/dist/lib/adapters/version/3.1/index.d.ts +3 -0
  16. package/dist/lib/adapters/version/3.1/index.js +36 -0
  17. package/dist/lib/adapters/version/3.1/index.mjs +12 -0
  18. package/dist/lib/adapters/version/index.d.mts +59 -7
  19. package/dist/lib/adapters/version/index.d.ts +59 -7
  20. package/dist/lib/adapters/version/index.js +51 -2
  21. package/dist/lib/adapters/version/index.mjs +49 -1
  22. package/dist/lib/adapters/version/negotiate.d.mts +71 -0
  23. package/dist/lib/adapters/version/negotiate.d.ts +71 -0
  24. package/dist/lib/adapters/version/negotiate.js +101 -0
  25. package/dist/lib/adapters/version/negotiate.mjs +73 -0
  26. package/dist/lib/adapters/version/schema-projection.d.mts +33 -0
  27. package/dist/lib/adapters/version/schema-projection.d.ts +33 -0
  28. package/dist/lib/adapters/version/schema-projection.js +294 -0
  29. package/dist/lib/adapters/version/schema-projection.mjs +269 -0
  30. package/dist/lib/adapters/version/translate.d.mts +122 -0
  31. package/dist/lib/adapters/version/translate.d.ts +122 -0
  32. package/dist/lib/adapters/version/translate.js +169 -0
  33. package/dist/lib/adapters/version/translate.mjs +141 -0
  34. package/dist/lib/adapters/version/types.d.mts +51 -10
  35. package/dist/lib/adapters/version/types.d.ts +51 -10
  36. package/dist/lib/core/AgentClient.d.mts +1 -1
  37. package/dist/lib/core/AgentClient.d.ts +1 -1
  38. package/dist/lib/core/AgentClient.js +17 -1
  39. package/dist/lib/core/AgentClient.mjs +17 -1
  40. package/dist/lib/core/ConversationTypes.d.mts +7 -0
  41. package/dist/lib/core/ConversationTypes.d.ts +7 -0
  42. package/dist/lib/core/SingleAgentClient.d.mts +82 -0
  43. package/dist/lib/core/SingleAgentClient.d.ts +82 -0
  44. package/dist/lib/core/SingleAgentClient.js +336 -15
  45. package/dist/lib/core/SingleAgentClient.mjs +350 -17
  46. package/dist/lib/core/TaskExecutor.d.mts +1 -1
  47. package/dist/lib/core/TaskExecutor.d.ts +1 -1
  48. package/dist/lib/core/TaskExecutor.js +182 -31
  49. package/dist/lib/core/TaskExecutor.mjs +183 -32
  50. package/dist/lib/core/account-reduction.d.mts +36 -0
  51. package/dist/lib/core/account-reduction.d.ts +36 -0
  52. package/dist/lib/core/account-reduction.js +127 -0
  53. package/dist/lib/core/account-reduction.mjs +98 -0
  54. package/dist/lib/core/buyer-account-registry.d.mts +52 -0
  55. package/dist/lib/core/buyer-account-registry.d.ts +52 -0
  56. package/dist/lib/core/buyer-account-registry.js +142 -6
  57. package/dist/lib/core/buyer-account-registry.mjs +142 -6
  58. package/dist/lib/errors/index.d.mts +34 -0
  59. package/dist/lib/errors/index.d.ts +34 -0
  60. package/dist/lib/errors/index.js +11 -0
  61. package/dist/lib/errors/index.mjs +10 -0
  62. package/dist/lib/index.d.mts +1 -1
  63. package/dist/lib/index.d.ts +1 -1
  64. package/dist/lib/index.js +2 -0
  65. package/dist/lib/index.mjs +2 -0
  66. package/dist/lib/media-buy/action-contracts.js +4 -4
  67. package/dist/lib/media-buy/action-contracts.mjs +4 -4
  68. package/dist/lib/media-buy/action-types.d.mts +3 -2
  69. package/dist/lib/media-buy/action-types.d.ts +3 -2
  70. package/dist/lib/media-buy/compatibility.d.mts +1 -0
  71. package/dist/lib/media-buy/compatibility.d.ts +1 -0
  72. package/dist/lib/media-buy/compatibility.js +33 -10
  73. package/dist/lib/media-buy/compatibility.mjs +32 -14
  74. package/dist/lib/media-buy/update-fields.generated.d.mts +3 -2
  75. package/dist/lib/media-buy/update-fields.generated.d.ts +3 -2
  76. package/dist/lib/negotiation/buyer.js +11 -5
  77. package/dist/lib/negotiation/buyer.mjs +11 -5
  78. package/dist/lib/negotiation/types.d.mts +4 -2
  79. package/dist/lib/negotiation/types.d.ts +4 -2
  80. package/dist/lib/negotiation/verification.d.mts +15 -6
  81. package/dist/lib/negotiation/verification.d.ts +15 -6
  82. package/dist/lib/negotiation/verification.js +146 -27
  83. package/dist/lib/negotiation/verification.mjs +146 -28
  84. package/dist/lib/negotiation/version.d.mts +6 -0
  85. package/dist/lib/negotiation/version.d.ts +6 -0
  86. package/dist/lib/negotiation/version.js +60 -0
  87. package/dist/lib/negotiation/version.mjs +33 -0
  88. package/dist/lib/registry/types.generated.d.mts +1 -1
  89. package/dist/lib/registry/types.generated.d.ts +1 -1
  90. package/dist/lib/server/create-adcp-server.d.mts +21 -0
  91. package/dist/lib/server/create-adcp-server.d.ts +21 -0
  92. package/dist/lib/server/create-adcp-server.js +98 -16
  93. package/dist/lib/server/create-adcp-server.mjs +84 -2
  94. package/dist/lib/server/error-arm-tools.d.mts +10 -10
  95. package/dist/lib/server/error-arm-tools.d.ts +10 -10
  96. package/dist/lib/server/error-arm-tools.js +22 -58
  97. package/dist/lib/server/error-arm-tools.mjs +30 -65
  98. package/dist/lib/server/index.d.mts +1 -1
  99. package/dist/lib/server/index.d.ts +1 -1
  100. package/dist/lib/storage/interfaces.d.mts +2 -0
  101. package/dist/lib/storage/interfaces.d.ts +2 -0
  102. package/dist/lib/testing/compliance/comply.js +17 -0
  103. package/dist/lib/testing/compliance/comply.mjs +17 -0
  104. package/dist/lib/testing/index.d.mts +3 -0
  105. package/dist/lib/testing/index.d.ts +3 -0
  106. package/dist/lib/testing/index.js +44 -2
  107. package/dist/lib/testing/index.mjs +42 -0
  108. package/dist/lib/testing/storyboard/collection-selection.d.mts +259 -0
  109. package/dist/lib/testing/storyboard/collection-selection.d.ts +259 -0
  110. package/dist/lib/testing/storyboard/collection-selection.js +384 -0
  111. package/dist/lib/testing/storyboard/collection-selection.mjs +348 -0
  112. package/dist/lib/testing/storyboard/context.js +12 -6
  113. package/dist/lib/testing/storyboard/context.mjs +12 -6
  114. package/dist/lib/testing/storyboard/fixture-resolution.d.mts +39 -1
  115. package/dist/lib/testing/storyboard/fixture-resolution.d.ts +39 -1
  116. package/dist/lib/testing/storyboard/fixture-resolution.js +216 -12
  117. package/dist/lib/testing/storyboard/fixture-resolution.mjs +218 -12
  118. package/dist/lib/testing/storyboard/index.d.mts +9 -2
  119. package/dist/lib/testing/storyboard/index.d.ts +9 -2
  120. package/dist/lib/testing/storyboard/index.js +51 -2
  121. package/dist/lib/testing/storyboard/index.mjs +49 -1
  122. package/dist/lib/testing/storyboard/prepared-fixtures.d.mts +471 -0
  123. package/dist/lib/testing/storyboard/prepared-fixtures.d.ts +471 -0
  124. package/dist/lib/testing/storyboard/prepared-fixtures.js +434 -0
  125. package/dist/lib/testing/storyboard/prepared-fixtures.mjs +389 -0
  126. package/dist/lib/testing/storyboard/protected-context.d.mts +127 -0
  127. package/dist/lib/testing/storyboard/protected-context.d.ts +127 -0
  128. package/dist/lib/testing/storyboard/protected-context.js +301 -0
  129. package/dist/lib/testing/storyboard/protected-context.mjs +267 -0
  130. package/dist/lib/testing/storyboard/rejection-hints.js +13 -4
  131. package/dist/lib/testing/storyboard/rejection-hints.mjs +13 -4
  132. package/dist/lib/testing/storyboard/runner.d.mts +24 -0
  133. package/dist/lib/testing/storyboard/runner.d.ts +24 -0
  134. package/dist/lib/testing/storyboard/runner.js +1061 -105
  135. package/dist/lib/testing/storyboard/runner.mjs +1075 -106
  136. package/dist/lib/testing/storyboard/seeding.d.mts +15 -1
  137. package/dist/lib/testing/storyboard/seeding.d.ts +15 -1
  138. package/dist/lib/testing/storyboard/seeding.js +272 -12
  139. package/dist/lib/testing/storyboard/seeding.mjs +274 -13
  140. package/dist/lib/testing/storyboard/types.d.mts +207 -10
  141. package/dist/lib/testing/storyboard/types.d.ts +207 -10
  142. package/dist/lib/testing/storyboard/types.js +1 -1
  143. package/dist/lib/testing/storyboard/types.mjs +1 -1
  144. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  145. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  146. package/dist/lib/testing/storyboard/validations.js +95 -0
  147. package/dist/lib/testing/storyboard/validations.mjs +95 -0
  148. package/dist/lib/validation/bundled-schema-store.js +5 -0
  149. package/dist/lib/validation/bundled-schema-store.mjs +4 -0
  150. package/dist/lib/validation/schema-loader.js +19 -2
  151. package/dist/lib/validation/schema-loader.mjs +22 -3
  152. package/dist/lib/version.d.mts +3 -3
  153. package/dist/lib/version.d.ts +3 -3
  154. package/dist/lib/version.js +3 -3
  155. package/dist/lib/version.mjs +3 -3
  156. package/docs/TYPE-SUMMARY.md +2 -2
  157. package/docs/guides/COLLECTION-FIXTURE-FOUNDATION.md +850 -0
  158. package/docs/guides/PROPOSAL-TERMS-VERIFICATION.md +66 -0
  159. package/docs/llms.txt +4 -3
  160. package/docs/migration-14.x-rc-worksheet.md +4 -4
  161. package/package.json +1 -1
@@ -22,7 +22,8 @@
22
22
  */
23
23
  import { type TestClient } from '../client.mjs';
24
24
  import { FixtureBindingRegistry } from './fixture-resolution.mjs';
25
- import type { FixtureResolutionRecord, Storyboard, StoryboardContext, StoryboardFixtures, StoryboardPhaseResult, StoryboardRunOptions } from './types.mjs';
25
+ import { type CollectionSelectionWitness } from './collection-selection.mjs';
26
+ import type { ContextProvenanceEntry, FixtureResolutionRecord, Storyboard, StoryboardContext, StoryboardFixtures, StoryboardPhaseResult, StoryboardRunOptions } from './types.mjs';
26
27
  /** Synthetic phase id used in `StoryboardResult.phases[]` for the seed pass. */
27
28
  export declare const CONTROLLER_SEEDING_PHASE_ID = "__controller_seeding__";
28
29
  /** Seed scenario names. Kept local — the server-side `SEED_SCENARIOS`
@@ -75,6 +76,19 @@ export interface ControllerSeedingResult {
75
76
  bindings?: FixtureBindingRegistry;
76
77
  /** Machine-readable AdCP 3.2 resolution evidence. */
77
78
  resolutionRecords?: FixtureResolutionRecord[];
79
+ /**
80
+ * Declaration-local captures from the SAME selected graph, published only
81
+ * when every declared fixture resolved. The runner commits them through the
82
+ * protected-context funnel; they are never merged by plain assignment.
83
+ */
84
+ preparedCaptures?: PreparedFixtureCaptures;
85
+ }
86
+ /** Reserved-namespace values and provenance produced by fixture preparation. */
87
+ export interface PreparedFixtureCaptures {
88
+ values: Record<string, unknown>;
89
+ provenance: Record<string, ContextProvenanceEntry>;
90
+ /** Per-handle witness retained for the entity that was actually selected. */
91
+ witnesses: Record<string, CollectionSelectionWitness>;
78
92
  }
79
93
  /**
80
94
  * Fire every seed call for this storyboard. Returns `null` when seeding is
@@ -22,7 +22,8 @@
22
22
  */
23
23
  import { type TestClient } from '../client';
24
24
  import { FixtureBindingRegistry } from './fixture-resolution';
25
- import type { FixtureResolutionRecord, Storyboard, StoryboardContext, StoryboardFixtures, StoryboardPhaseResult, StoryboardRunOptions } from './types';
25
+ import { type CollectionSelectionWitness } from './collection-selection';
26
+ import type { ContextProvenanceEntry, FixtureResolutionRecord, Storyboard, StoryboardContext, StoryboardFixtures, StoryboardPhaseResult, StoryboardRunOptions } from './types';
26
27
  /** Synthetic phase id used in `StoryboardResult.phases[]` for the seed pass. */
27
28
  export declare const CONTROLLER_SEEDING_PHASE_ID = "__controller_seeding__";
28
29
  /** Seed scenario names. Kept local — the server-side `SEED_SCENARIOS`
@@ -75,6 +76,19 @@ export interface ControllerSeedingResult {
75
76
  bindings?: FixtureBindingRegistry;
76
77
  /** Machine-readable AdCP 3.2 resolution evidence. */
77
78
  resolutionRecords?: FixtureResolutionRecord[];
79
+ /**
80
+ * Declaration-local captures from the SAME selected graph, published only
81
+ * when every declared fixture resolved. The runner commits them through the
82
+ * protected-context funnel; they are never merged by plain assignment.
83
+ */
84
+ preparedCaptures?: PreparedFixtureCaptures;
85
+ }
86
+ /** Reserved-namespace values and provenance produced by fixture preparation. */
87
+ export interface PreparedFixtureCaptures {
88
+ values: Record<string, unknown>;
89
+ provenance: Record<string, ContextProvenanceEntry>;
90
+ /** Per-handle witness retained for the entity that was actually selected. */
91
+ witnesses: Record<string, CollectionSelectionWitness>;
78
92
  }
79
93
  /**
80
94
  * Fire every seed call for this storyboard. Returns `null` when seeding is
@@ -30,6 +30,9 @@ var import_envelope_status_compat = require('../../utils/envelope-status-compat.
30
30
  var import_test_controller = require('../test-controller.js');
31
31
  var import_task_map = require('./task-map.js');
32
32
  var import_fixture_resolution = require('./fixture-resolution.js');
33
+ var import_collection_selection = require('./collection-selection.js');
34
+ var import_path = require('./path.js');
35
+ var import_redact_secrets = require('../../utils/redact-secrets.js');
33
36
  const CONTROLLER_SEEDING_PHASE_ID = "__controller_seeding__";
34
37
  function buildSeedCalls(fixtures) {
35
38
  if (!fixtures) return [];
@@ -379,6 +382,10 @@ async function runRoutedControllerSeeding(storyboard, calls, context, resolveFix
379
382
  failedCount
380
383
  };
381
384
  }
385
+ function discoveryExchangeFor(catalog, productId) {
386
+ const index = productId === void 0 ? void 0 : catalog.pageOfProduct.get(productId);
387
+ return catalog.exchanges[index ?? catalog.exchanges.length - 1];
388
+ }
382
389
  async function runDeclaredFixtureResolution(seedClient, discoveryClient, storyboard, options, context, calls, resolveFixtureAgent) {
383
390
  const start = Date.now();
384
391
  const authoringErrorResult = buildAuthoringErrorResult(storyboard, calls, context, start);
@@ -407,6 +414,11 @@ async function runDeclaredFixtureResolution(seedClient, discoveryClient, storybo
407
414
  const claimedProducts = /* @__PURE__ */ new Map();
408
415
  const claimedPricingOptions = /* @__PURE__ */ new Map();
409
416
  const productStatuses = /* @__PURE__ */ new Map();
417
+ const pendingCaptures = [];
418
+ const retainedWitnesses = /* @__PURE__ */ new Map();
419
+ let captureFailure;
420
+ const captureFailures = /* @__PURE__ */ new Map();
421
+ const captureFailedHandles = /* @__PURE__ */ new Set();
410
422
  const seedContext = { correlation_id: `${storyboardCorrelationPrefix(storyboard)}--__seeding__` };
411
423
  let seedOptions = options;
412
424
  let discoveryOptions = options;
@@ -539,6 +551,43 @@ async function runDeclaredFixtureResolution(seedClient, discoveryClient, storybo
539
551
  const data = raw.data;
540
552
  if (raw.success && data?.success === true) {
541
553
  boundIds = bindSeededFixture(spec, bindings);
554
+ const seedSnapshot = {
555
+ ...spec.fixture,
556
+ ...spec.entityType === "product" ? { product_id: boundIds.product_id } : { pricing_option_id: boundIds.pricing_option_id }
557
+ };
558
+ const seedWitness = (0, import_fixture_resolution.matchFixtureRequirementsWithWitness)(seedSnapshot, spec.clauses).witness;
559
+ if (seedWitness) retainedWitnesses.set(spec.handle, seedWitness);
560
+ const seedCaptures = collectDeclaredCaptures(spec, seedSnapshot, seedWitness);
561
+ if (!seedCaptures.ok) {
562
+ failure = seedCaptures.detail;
563
+ captureFailure ??= seedCaptures.detail;
564
+ captureFailures.set(spec.handle, {
565
+ ...seedCaptures.path !== void 0 && { pointer: toJsonPointer(seedCaptures.path) },
566
+ exchange: {
567
+ request: {
568
+ transport: "mcp",
569
+ operation: "comply_test_controller",
570
+ // The object actually handed to `callControllerRaw`,
571
+ // not `controllerRequest` — the call substitutes
572
+ // `call.params` when the bound params are not a plain
573
+ // record, and adds the seeding correlation context.
574
+ payload: (0, import_redact_secrets.redactSecrets)({
575
+ scenario: call.scenario,
576
+ params: isPlainRecord(controllerRequest.params) ? controllerRequest.params : call.params,
577
+ context: seedContext
578
+ })
579
+ },
580
+ response: { transport: "mcp", payload: (0, import_redact_secrets.redactSecrets)(raw.data ?? raw.error ?? null) }
581
+ }
582
+ });
583
+ captureFailedHandles.add(spec.handle);
584
+ attempts.push({ strategy, disposition: "failed", detail: seedCaptures.detail });
585
+ boundIds = void 0;
586
+ break;
587
+ }
588
+ pendingCaptures.push(
589
+ ...seedCaptures.captures.map((c) => ({ ...c, sourceTask: "comply_test_controller" }))
590
+ );
542
591
  if (spec.entityType === "product") {
543
592
  recordIdentityClaim(claimedProducts, boundIds.product_id, spec.allowReuse);
544
593
  } else {
@@ -594,7 +643,10 @@ async function runDeclaredFixtureResolution(seedClient, discoveryClient, storybo
594
643
  discovered.catalog.products,
595
644
  bindings,
596
645
  claimedProducts,
597
- claimedPricingOptions
646
+ claimedPricingOptions,
647
+ spec.entityType === "product" ? specs.filter(
648
+ (candidate) => candidate.entityType === "product_pricing_option" && candidate.parentProductHandle === spec.handle
649
+ ) : []
598
650
  );
599
651
  if (!selected.ok) {
600
652
  if (selected.failed) {
@@ -617,6 +669,27 @@ async function runDeclaredFixtureResolution(seedClient, discoveryClient, storybo
617
669
  }
618
670
  boundIds = selected.boundIds;
619
671
  chosenStrategy = strategy;
672
+ if (selected.witness) retainedWitnesses.set(spec.handle, selected.witness);
673
+ const collected = collectDeclaredCaptures(spec, selected.snapshot, selected.witness);
674
+ if (!collected.ok) {
675
+ failure = collected.detail;
676
+ captureFailure ??= collected.detail;
677
+ const failureExchange = discoveryExchangeFor(discovered.catalog, selected.boundIds.product_id);
678
+ captureFailures.set(spec.handle, {
679
+ ...collected.path !== void 0 && { pointer: toJsonPointer(collected.path) },
680
+ ...failureExchange && { exchange: failureExchange }
681
+ });
682
+ captureFailedHandles.add(spec.handle);
683
+ attempts.push({
684
+ strategy,
685
+ disposition: "failed",
686
+ detail: collected.detail,
687
+ response: { ...discovered.catalog.evidence, selected: selected.boundIds }
688
+ });
689
+ boundIds = void 0;
690
+ break;
691
+ }
692
+ pendingCaptures.push(...collected.captures.map((c) => ({ ...c, sourceTask: "get_products" })));
620
693
  attempts.push({
621
694
  strategy,
622
695
  disposition: "resolved",
@@ -640,6 +713,7 @@ async function runDeclaredFixtureResolution(seedClient, discoveryClient, storybo
640
713
  };
641
714
  records.push(record);
642
715
  if (status !== "unsatisfied") {
716
+ const captureFailed = status === "failed" && captureFailedHandles.has(spec.handle);
643
717
  steps.push({
644
718
  storyboard_id: storyboard.id,
645
719
  step_id: call.step_id,
@@ -648,7 +722,29 @@ async function runDeclaredFixtureResolution(seedClient, discoveryClient, storybo
648
722
  task: chosenStrategy === "discover" ? "get_products" : "comply_test_controller",
649
723
  passed: status !== "failed",
650
724
  duration_ms: Date.now() - stepStart,
651
- validations: [],
725
+ // An unbuildable capture gets a `check` discriminator like the other
726
+ // two failure classes, so a report renderer keys on `check` for all
727
+ // three instead of regexing prose for this one.
728
+ validations: captureFailed ? [
729
+ {
730
+ check: "fixture_capture_unbuildable",
731
+ passed: false,
732
+ description: "Every declared fixture capture must be buildable from the pinned entity",
733
+ error: failure,
734
+ // The contract's analogue, `capture_path_not_resolvable`,
735
+ // requires the RFC 6901 form of the `context_outputs` path
736
+ // that failed. `null` only for the witness and size/depth
737
+ // arms, which genuinely have no path.
738
+ json_pointer: captureFailures.get(spec.handle)?.pointer ?? null,
739
+ expected: "a value at the declared selected-entity path, or a derivable witness",
740
+ actual: null,
741
+ // required_on_failure: null only when the check is not a
742
+ // response_schema check, which this never is.
743
+ schema_id: null,
744
+ schema_url: null,
745
+ ...captureFailures.get(spec.handle)?.exchange ?? {}
746
+ }
747
+ ] : [],
652
748
  context,
653
749
  extraction: { path: "none" },
654
750
  ...failure && { error: failure }
@@ -659,6 +755,23 @@ async function runDeclaredFixtureResolution(seedClient, discoveryClient, storybo
659
755
  else passedCount++;
660
756
  }
661
757
  const allPassed = failedCount === 0;
758
+ const graphComplete = allPassed && !fixtureUnsatisfied && captureFailure === void 0 && records.every((r) => r.status === "resolved");
759
+ const preparedCaptures = graphComplete && pendingCaptures.length > 0 ? {
760
+ values: Object.fromEntries(pendingCaptures.map((entry) => [entry.key, entry.value])),
761
+ provenance: Object.fromEntries(
762
+ pendingCaptures.map((entry) => [
763
+ entry.key,
764
+ {
765
+ source_step_id: CONTROLLER_SEEDING_PHASE_ID,
766
+ source_kind: "fixture_preparation",
767
+ source_task: entry.sourceTask,
768
+ fixture_handle: entry.spec.handle,
769
+ ...entry.spec.parentProductHandle && { fixture_product_handle: entry.spec.parentProductHandle }
770
+ }
771
+ ])
772
+ ),
773
+ witnesses: Object.fromEntries(retainedWitnesses)
774
+ } : void 0;
662
775
  return {
663
776
  phase: {
664
777
  phase_id: CONTROLLER_SEEDING_PHASE_ID,
@@ -672,8 +785,76 @@ async function runDeclaredFixtureResolution(seedClient, discoveryClient, storybo
672
785
  failedCount,
673
786
  fixtureUnsatisfied,
674
787
  bindings,
675
- resolutionRecords: records
788
+ resolutionRecords: records,
789
+ ...preparedCaptures && { preparedCaptures }
790
+ };
791
+ }
792
+ function toJsonPointer(path) {
793
+ if (path === void 0 || path.length === 0) return void 0;
794
+ const segments = path.replace(/\[(\d+)\]/g, ".$1").split(".").filter((segment) => segment.length > 0).map((segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1"));
795
+ return segments.length > 0 ? `/${segments.join("/")}` : void 0;
796
+ }
797
+ const DISCOVERY_RETAINED_RESPONSE_PAGES = 16;
798
+ const CAPTURE_MAX_DEPTH = 64;
799
+ const CAPTURE_MAX_BYTES = 512 * 1024;
800
+ function describeUncapturableValue(value, key) {
801
+ let depth = 0;
802
+ const walk = (node, level) => {
803
+ if (level > depth) depth = level;
804
+ if (depth > CAPTURE_MAX_DEPTH) return false;
805
+ if (node === null || typeof node !== "object") return true;
806
+ if (Array.isArray(node)) return node.every((item) => walk(item, level + 1));
807
+ return Object.values(node).every((item) => walk(item, level + 1));
676
808
  };
809
+ if (!walk(value, 1)) {
810
+ return `fixture capture "${key}" nests deeper than the ${CAPTURE_MAX_DEPTH}-level capture limit`;
811
+ }
812
+ let size;
813
+ try {
814
+ size = Buffer.byteLength(JSON.stringify(value) ?? "", "utf8");
815
+ } catch {
816
+ return `fixture capture "${key}" is not JSON-serializable and cannot be detached`;
817
+ }
818
+ if (size > CAPTURE_MAX_BYTES) {
819
+ return `fixture capture "${key}" serializes to ${size} bytes, over the ${CAPTURE_MAX_BYTES}-byte capture limit`;
820
+ }
821
+ return void 0;
822
+ }
823
+ function collectDeclaredCaptures(spec, snapshot, witness) {
824
+ if (spec.contextOutputs.length === 0) return { ok: true, captures: [] };
825
+ const captures = [];
826
+ for (const output of spec.contextOutputs) {
827
+ if (output.snapshot !== void 0) {
828
+ captures.push({ spec, key: output.key, value: (0, import_redact_secrets.redactSecrets)(snapshot) });
829
+ continue;
830
+ }
831
+ if (output.witness !== void 0) {
832
+ const read = (0, import_collection_selection.readCollectionWitness)(witness, output.witness);
833
+ if (!read.ok) {
834
+ return {
835
+ ok: false,
836
+ detail: `fixture capture "${output.key}" for handle "${spec.handle}" could not be built: ${read.detail}`
837
+ };
838
+ }
839
+ captures.push({ spec, key: output.key, value: read.value });
840
+ continue;
841
+ }
842
+ const value = (0, import_path.resolvePath)((0, import_redact_secrets.redactSecrets)(snapshot), output.path);
843
+ if (value === void 0 || value === null || value === "") {
844
+ 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}"`;
845
+ return {
846
+ ok: false,
847
+ path: output.path,
848
+ 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.`
849
+ };
850
+ }
851
+ captures.push({ spec, key: output.key, value });
852
+ }
853
+ for (const capture of captures) {
854
+ const unusable = describeUncapturableValue(capture.value, capture.key);
855
+ if (unusable) return { ok: false, detail: unusable };
856
+ }
857
+ return { ok: true, captures };
677
858
  }
678
859
  function resolutionSpecKey(spec) {
679
860
  return spec.entityType === "product" ? `seed_product\0${spec.handle}` : `seed_pricing_option\0${spec.parentProductHandle}\0${spec.handle}`;
@@ -779,6 +960,8 @@ async function discoverProductCatalog(client, options, context) {
779
960
  const seenCursors = /* @__PURE__ */ new Set();
780
961
  let cursor;
781
962
  let pageCount = 0;
963
+ const exchanges = [];
964
+ const pageOfProduct = /* @__PURE__ */ new Map();
782
965
  do {
783
966
  if (pageCount >= 1e3) {
784
967
  return { ok: false, error: "fixture discovery pagination exceeded the 1,000-page safety limit" };
@@ -792,6 +975,7 @@ async function discoverProductCatalog(client, options, context) {
792
975
  ext: { adcp: { creative_wire: "canonical" } }
793
976
  };
794
977
  let result;
978
+ const pageStart = Date.now();
795
979
  try {
796
980
  result = await (0, import_task_map.executeStoryboardTask)(client, "get_products", request, { signal: options.signal });
797
981
  } catch (err) {
@@ -801,6 +985,19 @@ async function discoverProductCatalog(client, options, context) {
801
985
  };
802
986
  }
803
987
  pageCount++;
988
+ const retainResponse = exchanges.length < DISCOVERY_RETAINED_RESPONSE_PAGES;
989
+ const pageIndex = exchanges.push({
990
+ request: { transport: "mcp", operation: "get_products", payload: (0, import_redact_secrets.redactSecrets)(request) },
991
+ response: {
992
+ transport: "mcp",
993
+ payload: retainResponse ? (0, import_redact_secrets.redactSecrets)(
994
+ result.data ?? (result.adcp_error ? { adcp_error: result.adcp_error } : void 0) ?? result.error ?? null
995
+ ) : {
996
+ 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`
997
+ },
998
+ duration_ms: Date.now() - pageStart
999
+ }
1000
+ }) - 1;
804
1001
  if (!result.success) {
805
1002
  return {
806
1003
  ok: false,
@@ -862,6 +1059,7 @@ async function discoverProductCatalog(client, options, context) {
862
1059
  seenPricingIds.add(option.pricing_option_id);
863
1060
  }
864
1061
  }
1062
+ pageOfProduct.set(product.product_id, pageIndex);
865
1063
  products.push(product);
866
1064
  if (products.length > 1e5) {
867
1065
  return { ok: false, error: "fixture discovery exceeded the 100,000-product safety limit" };
@@ -885,7 +1083,12 @@ async function discoverProductCatalog(client, options, context) {
885
1083
  } while (cursor);
886
1084
  return {
887
1085
  ok: true,
888
- catalog: { products, evidence: { page_count: pageCount, product_count: products.length, cursors } }
1086
+ catalog: {
1087
+ products,
1088
+ evidence: { page_count: pageCount, product_count: products.length, cursors },
1089
+ exchanges,
1090
+ pageOfProduct
1091
+ }
889
1092
  };
890
1093
  }
891
1094
  function isPlainRecord(value) {
@@ -894,19 +1097,47 @@ function isPlainRecord(value) {
894
1097
  function utf8Compare(a, b) {
895
1098
  return Buffer.compare(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
896
1099
  }
897
- function selectDiscoveredFixture(spec, products, bindings, claimedProducts, claimedPricingOptions) {
1100
+ function selectDiscoveredFixture(spec, products, bindings, claimedProducts, claimedPricingOptions, dependentPricingSpecs = []) {
898
1101
  if (spec.entityType === "product") {
899
- const candidates2 = products.filter((product2) => (0, import_fixture_resolution.matchesFixtureRequirements)(product2, spec.clauses)).filter((product2) => identityCanBeClaimed(claimedProducts, product2.product_id, spec.allowReuse)).sort((a, b) => utf8Compare(a.product_id, b.product_id));
900
- const selected2 = candidates2[0];
901
- if (!selected2)
902
- return { ok: false, failed: false, detail: "no discovered product satisfied the authored requirements" };
1102
+ const rejections = /* @__PURE__ */ new Map();
1103
+ let claimedElsewhere = 0;
1104
+ let strandedChild = 0;
1105
+ const matched = [];
1106
+ for (const product2 of products) {
1107
+ const match = (0, import_fixture_resolution.matchFixtureRequirementsWithWitness)(product2, spec.clauses);
1108
+ if (!match.matched) {
1109
+ const reason = match.reason ?? "did not satisfy the authored match predicates";
1110
+ rejections.set(reason, (rejections.get(reason) ?? 0) + 1);
1111
+ continue;
1112
+ }
1113
+ if (!identityCanBeClaimed(claimedProducts, product2.product_id, spec.allowReuse)) {
1114
+ claimedElsewhere++;
1115
+ continue;
1116
+ }
1117
+ if (!dependentPricingSpecs.every((child) => productCanSatisfyPricingSpec(product2, child))) {
1118
+ strandedChild++;
1119
+ continue;
1120
+ }
1121
+ matched.push({ product: product2, ...match.witness && { witness: match.witness } });
1122
+ }
1123
+ matched.sort((a, b) => utf8Compare(a.product.product_id, b.product.product_id));
1124
+ const selected2 = matched[0]?.product;
1125
+ if (!selected2) {
1126
+ return {
1127
+ ok: false,
1128
+ failed: false,
1129
+ detail: describeProductNoMatch(spec, products.length, rejections, claimedElsewhere, strandedChild)
1130
+ };
1131
+ }
903
1132
  const productId = selected2.product_id;
904
1133
  bindings.bindProduct(spec.handle, productId);
905
1134
  recordIdentityClaim(claimedProducts, productId, spec.allowReuse);
906
1135
  return {
907
1136
  ok: true,
908
1137
  boundIds: { product_id: productId },
909
- detail: `selected product_id "${productId}" by UTF-8 bytewise order`
1138
+ detail: `selected product_id "${productId}" by UTF-8 bytewise order`,
1139
+ snapshot: selected2,
1140
+ ...matched[0].witness && { witness: matched[0].witness }
910
1141
  };
911
1142
  }
912
1143
  const parentHandle = spec.parentProductHandle;
@@ -941,7 +1172,7 @@ function selectDiscoveredFixture(spec, products, bindings, claimedProducts, clai
941
1172
  return {
942
1173
  ok: false,
943
1174
  failed: false,
944
- detail: "no pricing option on the bound product satisfied the authored requirements"
1175
+ 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(", ")}` : "")
945
1176
  };
946
1177
  }
947
1178
  const pricingOptionId = selected.pricing_option_id;
@@ -950,9 +1181,38 @@ function selectDiscoveredFixture(spec, products, bindings, claimedProducts, clai
950
1181
  return {
951
1182
  ok: true,
952
1183
  boundIds: { product_id: sellerProductId, pricing_option_id: pricingOptionId },
953
- detail: `selected pricing option "${sellerProductId}\0${pricingOptionId}" by UTF-8 bytewise order`
1184
+ detail: `selected pricing option "${sellerProductId}\0${pricingOptionId}" by UTF-8 bytewise order`,
1185
+ // The scoped price comes out of the same pinned product graph, so a
1186
+ // captured currency / fixed_price / min_spend_per_package describes the
1187
+ // option the buy will actually reference.
1188
+ snapshot: selected
954
1189
  };
955
1190
  }
1191
+ function describeProductNoMatch(spec, candidateCount, rejections, claimedElsewhere, strandedChild) {
1192
+ const parts = [
1193
+ `no discovered product satisfied fixture handle "${spec.handle}" \u2014 ${candidateCount} candidate(s) enumerated from the complete paginated catalog`
1194
+ ];
1195
+ const top = [...rejections].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([reason, count]) => `${count}x ${reason}`);
1196
+ if (top.length > 0) parts.push(`rejected by the authored match: ${top.join(" | ")}`);
1197
+ if (claimedElsewhere > 0) {
1198
+ parts.push(
1199
+ `${claimedElsewhere} otherwise-matching product(s) were already pinned to another handle \u2014 set allow_reuse: true on this declaration to share one seller entity`
1200
+ );
1201
+ }
1202
+ if (strandedChild > 0) {
1203
+ parts.push(
1204
+ `${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`
1205
+ );
1206
+ }
1207
+ return parts.join(". ");
1208
+ }
1209
+ function productCanSatisfyPricingSpec(product, child) {
1210
+ if (child.strategies.length !== 1 || child.strategies[0] !== "discover") return true;
1211
+ const options = Array.isArray(product.pricing_options) ? product.pricing_options : [];
1212
+ return options.filter(isPlainRecord).some(
1213
+ (option) => typeof option.pricing_option_id === "string" && option.pricing_option_id.length > 0 && (0, import_fixture_resolution.matchesFixtureRequirements)(option, child.clauses)
1214
+ );
1215
+ }
956
1216
  function identityCanBeClaimed(claims, identity, allowReuse) {
957
1217
  const existingAllowsReuse = claims.get(identity);
958
1218
  return existingAllowsReuse === void 0 || existingAllowsReuse && allowReuse;