@adcp/sdk 16.2.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 (97) 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/index.d.mts +1 -1
  13. package/dist/lib/index.d.ts +1 -1
  14. package/dist/lib/index.js +2 -0
  15. package/dist/lib/index.mjs +2 -0
  16. package/dist/lib/media-buy/compatibility.d.mts +1 -0
  17. package/dist/lib/media-buy/compatibility.d.ts +1 -0
  18. package/dist/lib/media-buy/compatibility.js +33 -10
  19. package/dist/lib/media-buy/compatibility.mjs +32 -14
  20. package/dist/lib/negotiation/buyer.js +11 -5
  21. package/dist/lib/negotiation/buyer.mjs +11 -5
  22. package/dist/lib/negotiation/types.d.mts +4 -2
  23. package/dist/lib/negotiation/types.d.ts +4 -2
  24. package/dist/lib/negotiation/verification.d.mts +15 -6
  25. package/dist/lib/negotiation/verification.d.ts +15 -6
  26. package/dist/lib/negotiation/verification.js +146 -27
  27. package/dist/lib/negotiation/verification.mjs +146 -28
  28. package/dist/lib/negotiation/version.d.mts +6 -0
  29. package/dist/lib/negotiation/version.d.ts +6 -0
  30. package/dist/lib/negotiation/version.js +60 -0
  31. package/dist/lib/negotiation/version.mjs +33 -0
  32. package/dist/lib/server/create-adcp-server.js +1 -1
  33. package/dist/lib/server/create-adcp-server.mjs +1 -1
  34. package/dist/lib/server/error-arm-tools.d.mts +10 -10
  35. package/dist/lib/server/error-arm-tools.d.ts +10 -10
  36. package/dist/lib/server/error-arm-tools.js +22 -58
  37. package/dist/lib/server/error-arm-tools.mjs +30 -65
  38. package/dist/lib/testing/compliance/comply.js +17 -0
  39. package/dist/lib/testing/compliance/comply.mjs +17 -0
  40. package/dist/lib/testing/index.d.mts +3 -0
  41. package/dist/lib/testing/index.d.ts +3 -0
  42. package/dist/lib/testing/index.js +44 -2
  43. package/dist/lib/testing/index.mjs +42 -0
  44. package/dist/lib/testing/storyboard/collection-selection.d.mts +259 -0
  45. package/dist/lib/testing/storyboard/collection-selection.d.ts +259 -0
  46. package/dist/lib/testing/storyboard/collection-selection.js +384 -0
  47. package/dist/lib/testing/storyboard/collection-selection.mjs +348 -0
  48. package/dist/lib/testing/storyboard/context.js +12 -6
  49. package/dist/lib/testing/storyboard/context.mjs +12 -6
  50. package/dist/lib/testing/storyboard/fixture-resolution.d.mts +39 -1
  51. package/dist/lib/testing/storyboard/fixture-resolution.d.ts +39 -1
  52. package/dist/lib/testing/storyboard/fixture-resolution.js +216 -12
  53. package/dist/lib/testing/storyboard/fixture-resolution.mjs +218 -12
  54. package/dist/lib/testing/storyboard/index.d.mts +9 -2
  55. package/dist/lib/testing/storyboard/index.d.ts +9 -2
  56. package/dist/lib/testing/storyboard/index.js +51 -2
  57. package/dist/lib/testing/storyboard/index.mjs +49 -1
  58. package/dist/lib/testing/storyboard/prepared-fixtures.d.mts +471 -0
  59. package/dist/lib/testing/storyboard/prepared-fixtures.d.ts +471 -0
  60. package/dist/lib/testing/storyboard/prepared-fixtures.js +434 -0
  61. package/dist/lib/testing/storyboard/prepared-fixtures.mjs +389 -0
  62. package/dist/lib/testing/storyboard/protected-context.d.mts +127 -0
  63. package/dist/lib/testing/storyboard/protected-context.d.ts +127 -0
  64. package/dist/lib/testing/storyboard/protected-context.js +301 -0
  65. package/dist/lib/testing/storyboard/protected-context.mjs +267 -0
  66. package/dist/lib/testing/storyboard/rejection-hints.js +13 -4
  67. package/dist/lib/testing/storyboard/rejection-hints.mjs +13 -4
  68. package/dist/lib/testing/storyboard/runner.d.mts +24 -0
  69. package/dist/lib/testing/storyboard/runner.d.ts +24 -0
  70. package/dist/lib/testing/storyboard/runner.js +1061 -105
  71. package/dist/lib/testing/storyboard/runner.mjs +1075 -106
  72. package/dist/lib/testing/storyboard/seeding.d.mts +15 -1
  73. package/dist/lib/testing/storyboard/seeding.d.ts +15 -1
  74. package/dist/lib/testing/storyboard/seeding.js +272 -12
  75. package/dist/lib/testing/storyboard/seeding.mjs +274 -13
  76. package/dist/lib/testing/storyboard/types.d.mts +207 -10
  77. package/dist/lib/testing/storyboard/types.d.ts +207 -10
  78. package/dist/lib/testing/storyboard/types.js +1 -1
  79. package/dist/lib/testing/storyboard/types.mjs +1 -1
  80. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  81. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  82. package/dist/lib/testing/storyboard/validations.js +95 -0
  83. package/dist/lib/testing/storyboard/validations.mjs +95 -0
  84. package/dist/lib/validation/bundled-schema-store.js +5 -0
  85. package/dist/lib/validation/bundled-schema-store.mjs +4 -0
  86. package/dist/lib/validation/schema-loader.js +19 -2
  87. package/dist/lib/validation/schema-loader.mjs +22 -3
  88. package/dist/lib/version.d.mts +3 -3
  89. package/dist/lib/version.d.ts +3 -3
  90. package/dist/lib/version.js +3 -3
  91. package/dist/lib/version.mjs +3 -3
  92. package/docs/TYPE-SUMMARY.md +1 -1
  93. package/docs/guides/COLLECTION-FIXTURE-FOUNDATION.md +850 -0
  94. package/docs/guides/PROPOSAL-TERMS-VERIFICATION.md +66 -0
  95. package/docs/llms.txt +3 -2
  96. package/docs/migration-14.x-rc-worksheet.md +4 -4
  97. package/package.json +1 -1
@@ -21,7 +21,9 @@ __export(fixture_resolution_exports, {
21
21
  FixtureBindingRegistry: () => FixtureBindingRegistry,
22
22
  applyFixtureBindingsToRequest: () => applyFixtureBindingsToRequest,
23
23
  buildFixtureResolutionSpecs: () => buildFixtureResolutionSpecs,
24
+ matchFixtureRequirementsWithWitness: () => matchFixtureRequirementsWithWitness,
24
25
  matchesFixtureRequirements: () => matchesFixtureRequirements,
26
+ normalizeFixtureContextOutputs: () => normalizeFixtureContextOutputs,
25
27
  normalizeFixtureMatchExpression: () => normalizeFixtureMatchExpression,
26
28
  validateFixtureResolutionDeclarations: () => validateFixtureResolutionDeclarations
27
29
  });
@@ -30,8 +32,18 @@ var import_node_util = require("node:util");
30
32
  var import_version = require('../../version.js');
31
33
  var import_schema_loader = require('../../validation/schema-loader.js');
32
34
  var import_canonical_format_satisfaction = require('./canonical-format-satisfaction.js');
33
- const OPERATORS = ["equals", "present", "contains_all", "any_match", "canonical_format_satisfies"];
34
- const DECLARATION_KEYS = /* @__PURE__ */ new Set(["handle", "strategies", "match", "allow_reuse"]);
35
+ var import_collection_selection = require('./collection-selection.js');
36
+ var import_path = require('./path.js');
37
+ var import_protected_context = require('./protected-context.js');
38
+ const OPERATORS = [
39
+ "equals",
40
+ "present",
41
+ "contains_all",
42
+ "any_match",
43
+ "canonical_format_satisfies",
44
+ "collection_selection_satisfies"
45
+ ];
46
+ const DECLARATION_KEYS = /* @__PURE__ */ new Set(["handle", "strategies", "match", "allow_reuse", "context_outputs"]);
35
47
  const PRICING_DECLARATION_KEYS = /* @__PURE__ */ new Set([...DECLARATION_KEYS, "product_handle"]);
36
48
  function isRecord(value) {
37
49
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -103,6 +115,12 @@ function normalizeClause(value, location) {
103
115
  operator,
104
116
  where: normalizeFixtureMatchExpression(value.where, `${location}.where`)
105
117
  };
118
+ case "collection_selection_satisfies":
119
+ return {
120
+ path: value.path,
121
+ operator,
122
+ value: (0, import_collection_selection.normalizeCollectionSelectionSelector)(value.value, `${location}.value`)
123
+ };
106
124
  case "canonical_format_satisfies":
107
125
  assertPlainObject(value.value, `${location}.value`);
108
126
  if (typeof value.value.format_kind !== "string" || value.value.format_kind.length === 0) {
@@ -114,7 +132,7 @@ function normalizeClause(value, location) {
114
132
  return { path: value.path, operator, value: value.value };
115
133
  }
116
134
  }
117
- function validateDeclaration(declaration, location, allowedKeys) {
135
+ function validateDeclaration(declaration, location, allowedKeys, entityKind = "product") {
118
136
  assertPlainObject(declaration, location);
119
137
  const unknown = Object.keys(declaration).filter((key) => !allowedKeys.has(key));
120
138
  if (unknown.length > 0) throw new Error(`${location}: unknown key(s): ${unknown.join(", ")}`);
@@ -142,10 +160,87 @@ function validateDeclaration(declaration, location, allowedKeys) {
142
160
  if (Array.isArray(declaration.strategies) && declaration.strategies.includes("discover") && clauses.length === 0) {
143
161
  throw new Error(`${location}.match: discover strategy requires at least one authored predicate`);
144
162
  }
163
+ normalizeFixtureContextOutputs(declaration.context_outputs, `${location}.context_outputs`, clauses, entityKind);
164
+ }
165
+ const FIXTURE_CONTEXT_OUTPUT_KEYS = /* @__PURE__ */ new Set(["key", "path", "witness", "snapshot"]);
166
+ function normalizeFixtureContextOutputs(value, location, clauses, entityKind = "product") {
167
+ if (value === void 0) return [];
168
+ assertArray(value, location);
169
+ if (value.length === 0) throw new Error(`${location}: must contain at least one capture when present`);
170
+ const hasCollectionPredicate = clauses.some((clause) => clause.operator === "collection_selection_satisfies");
171
+ const seen = /* @__PURE__ */ new Set();
172
+ return value.map((raw, index) => {
173
+ const entryLocation = `${location}[${index}]`;
174
+ assertPlainObject(raw, entryLocation);
175
+ const unknown = Object.keys(raw).filter((key) => !FIXTURE_CONTEXT_OUTPUT_KEYS.has(key));
176
+ if (unknown.length > 0) {
177
+ throw new Error(`${entryLocation}: unknown key(s): ${unknown.join(", ")}`);
178
+ }
179
+ if (typeof raw.key !== "string" || raw.key.length === 0) {
180
+ throw new Error(`${entryLocation}.key: must be a non-empty string`);
181
+ }
182
+ if (!(0, import_protected_context.isProtectedContextKey)(raw.key)) {
183
+ throw new Error(
184
+ `${entryLocation}.key: fixture captures must use the reserved "__adcp_fixture_" prefix so later steps, caller context and seller responses cannot replace them; received "${raw.key}"`
185
+ );
186
+ }
187
+ (0, import_protected_context.assertWellFormedProtectedKey)(raw.key, entryLocation + ".key");
188
+ if (seen.has(raw.key)) throw new Error(`${entryLocation}.key: duplicate capture key "${raw.key}"`);
189
+ seen.add(raw.key);
190
+ const forms = ["path", "witness", "snapshot"].filter((form) => Object.hasOwn(raw, form));
191
+ if (forms.length !== 1) {
192
+ throw new Error(
193
+ `${entryLocation}: must set exactly one of 'path', 'witness' or 'snapshot'; received ${forms.length === 0 ? "none" : forms.join(" + ")}`
194
+ );
195
+ }
196
+ const hasPath = forms[0] === "path";
197
+ if (forms[0] === "snapshot") {
198
+ if (raw.snapshot !== "selected_entity") {
199
+ throw new Error(`${entryLocation}.snapshot: must be 'selected_entity'`);
200
+ }
201
+ return { key: raw.key, snapshot: "selected_entity" };
202
+ }
203
+ if (hasPath) {
204
+ if (typeof raw.path !== "string" || raw.path.length === 0) {
205
+ throw new Error(`${entryLocation}.path: must be a non-empty string`);
206
+ }
207
+ if (raw.path.startsWith("/")) {
208
+ throw new Error(
209
+ `${entryLocation}.path: capture paths use the dotted storyboard grammar ("currency", "pricing_options[0].rate"), not the RFC 6901 pointers that match[].path takes; drop the leading "/" \u2014 received "${raw.path}"`
210
+ );
211
+ }
212
+ if ((0, import_path.parsePath)(raw.path).length === 0) {
213
+ throw new Error(
214
+ `${entryLocation}.path: must name at least one field in the dotted storyboard grammar ("currency", "pricing_options[0].rate") \u2014 received "${raw.path}", which resolves to the whole selected entity. To capture the whole entity, say so: snapshot: 'selected_entity'.`
215
+ );
216
+ }
217
+ if (raw.path.split(".").some((segment) => segment.length === 0)) {
218
+ throw new Error(`${entryLocation}.path: every dotted segment must name a field \u2014 received "${raw.path}".`);
219
+ }
220
+ return { key: raw.key, path: raw.path };
221
+ }
222
+ if (typeof raw.witness !== "string" || !import_collection_selection.COLLECTION_WITNESS_KINDS.includes(raw.witness)) {
223
+ throw new Error(`${entryLocation}.witness: must be one of ${import_collection_selection.COLLECTION_WITNESS_KINDS.join(", ")}`);
224
+ }
225
+ if (entityKind === "pricing_option") {
226
+ throw new Error(
227
+ `${entryLocation}.witness: a collection witness describes the product, not one of its pricing options; declare it under fixture_resolution.products.<product_handle>.context_outputs instead`
228
+ );
229
+ }
230
+ if (!hasCollectionPredicate) {
231
+ throw new Error(
232
+ `${entryLocation}.witness: requires a collection_selection_satisfies predicate on the same declaration`
233
+ );
234
+ }
235
+ return { key: raw.key, witness: raw.witness };
236
+ });
145
237
  }
146
238
  function validateFixtureResolutionDeclarations(storyboard) {
147
239
  const root = storyboard.fixture_resolution;
148
- if (root === void 0) return;
240
+ if (root === void 0) {
241
+ assertNoReservedOrdinaryKeys(storyboard);
242
+ return;
243
+ }
149
244
  assertPlainObject(root, `[${storyboard.id}] fixture_resolution`);
150
245
  const unknownRoot = Object.keys(root).filter((key) => key !== "products" && key !== "pricing_options");
151
246
  if (unknownRoot.length > 0) {
@@ -195,7 +290,7 @@ function validateFixtureResolutionDeclarations(storyboard) {
195
290
  for (let index = 0; index < root.pricing_options.length; index++) {
196
291
  const declaration = root.pricing_options[index];
197
292
  const location = `[${storyboard.id}] fixture_resolution.pricing_options[${index}]`;
198
- validateDeclaration(declaration, location, PRICING_DECLARATION_KEYS);
293
+ validateDeclaration(declaration, location, PRICING_DECLARATION_KEYS, "pricing_option");
199
294
  const typedDeclaration = declaration;
200
295
  if (typeof typedDeclaration.product_handle !== "string" || typedDeclaration.product_handle.length === 0) {
201
296
  throw new Error(`${location}.product_handle: must be a non-empty string`);
@@ -212,6 +307,68 @@ function validateFixtureResolutionDeclarations(storyboard) {
212
307
  }
213
308
  }
214
309
  }
310
+ assertNoReservedOrdinaryKeys(storyboard);
311
+ assertUniqueFixtureCaptureKeys(storyboard);
312
+ }
313
+ function assertNoReservedOrdinaryKeys(storyboard) {
314
+ const reject = (key, location) => {
315
+ if ((0, import_protected_context.isProtectedContextKey)(key)) {
316
+ throw new Error(
317
+ `${location}: "${key}" uses the reserved "__adcp_fixture_" prefix, which is owned by fixture preparation. Rename it to any key not starting with "__adcp_fixture_".`
318
+ );
319
+ }
320
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
321
+ throw new Error(
322
+ `${location}: "${key}" is a prototype-sensitive context key and cannot be used; rename it to anything other than __proto__, constructor or prototype`
323
+ );
324
+ }
325
+ };
326
+ for (const key of Object.keys(storyboard.context ?? {})) {
327
+ reject(key, `[${storyboard.id}] context`);
328
+ }
329
+ for (const phase of storyboard.phases ?? []) {
330
+ for (const step of phase.steps ?? []) {
331
+ for (const output of step.context_outputs ?? []) {
332
+ if (typeof output.key !== "string") continue;
333
+ if ((0, import_protected_context.isProtectedContextKey)(output.key)) {
334
+ throw new Error(
335
+ `[${storyboard.id}] phase '${phase.id}' step '${step.id}': context_outputs entry "${output.key}" uses the reserved "__adcp_fixture_" prefix; step captures cannot write fixture-preparation state. Declare it under fixture_resolution instead, or rename the key.`
336
+ );
337
+ }
338
+ reject(output.key, `[${storyboard.id}] phase '${phase.id}' step '${step.id}' context_outputs`);
339
+ }
340
+ const gates = step.not_applicable_if;
341
+ for (const gate of gates === void 0 ? [] : Array.isArray(gates) ? gates : [gates]) {
342
+ for (const key of gate.context_keys ?? []) {
343
+ if (typeof key === "string") {
344
+ reject(key, `[${storyboard.id}] phase '${phase.id}' step '${step.id}' not_applicable_if.context_keys`);
345
+ }
346
+ }
347
+ }
348
+ }
349
+ }
350
+ }
351
+ function assertUniqueFixtureCaptureKeys(storyboard) {
352
+ const owners = /* @__PURE__ */ new Map();
353
+ const claim = (key, owner) => {
354
+ const existing = owners.get(key);
355
+ if (existing !== void 0) {
356
+ throw new Error(
357
+ `[${storyboard.id}] fixture_resolution: capture key "${key}" is declared by both ${existing} and ${owner}`
358
+ );
359
+ }
360
+ owners.set(key, owner);
361
+ };
362
+ for (const declaration of storyboard.fixture_resolution?.products ?? []) {
363
+ for (const output of declaration.context_outputs ?? []) {
364
+ claim(output.key, `products."${declaration.handle}"`);
365
+ }
366
+ }
367
+ for (const declaration of storyboard.fixture_resolution?.pricing_options ?? []) {
368
+ for (const output of declaration.context_outputs ?? []) {
369
+ claim(output.key, `pricing_options."${declaration.product_handle}/${declaration.handle}"`);
370
+ }
371
+ }
215
372
  }
216
373
  function resolveCandidatePath(candidate, pointer) {
217
374
  let current = candidate;
@@ -235,9 +392,38 @@ function matchesFixtureRequirements(candidate, clauses) {
235
392
  return Array.isArray(actual) && actual.some((value) => matchesFixtureRequirements(value, clause.where ?? []));
236
393
  case "canonical_format_satisfies":
237
394
  return (0, import_canonical_format_satisfaction.productCanonicalFormatSatisfies)(actual, clause.value);
395
+ case "collection_selection_satisfies":
396
+ return (0, import_collection_selection.evaluateCollectionSelection)(actual, clause.value).eligible;
238
397
  }
239
398
  });
240
399
  }
400
+ function matchFixtureRequirementsWithWitness(candidate, clauses) {
401
+ let witness;
402
+ let matched = true;
403
+ let reason;
404
+ for (const clause of clauses) {
405
+ if (clause.operator !== "collection_selection_satisfies") {
406
+ if (matched && !matchesFixtureRequirements(candidate, [clause])) {
407
+ matched = false;
408
+ reason = `${clause.operator} at "${clause.path || "(whole candidate)"}" did not match`;
409
+ }
410
+ continue;
411
+ }
412
+ const actual = resolveCandidatePath(candidate, clause.path);
413
+ const evaluation = (0, import_collection_selection.evaluateCollectionSelection)(actual, clause.value);
414
+ if (!evaluation.eligible) {
415
+ if (matched) reason = evaluation.reason;
416
+ matched = false;
417
+ continue;
418
+ }
419
+ witness = evaluation.witness;
420
+ }
421
+ return {
422
+ matched,
423
+ ...witness && { witness },
424
+ ...reason !== void 0 && { reason }
425
+ };
426
+ }
241
427
  function buildFixtureResolutionSpecs(storyboard) {
242
428
  const specs = [];
243
429
  const productFixtures = new Map(
@@ -260,13 +446,22 @@ function buildFixtureResolutionSpecs(storyboard) {
260
446
  if (!fixture) continue;
261
447
  if (typeof fixture.product_id !== "string" || fixture.product_id.length === 0) continue;
262
448
  const { product_id: handle, ...requirementFixture } = fixture;
449
+ const productClauses = normalizeFixtureMatchExpression(
450
+ declaration?.match,
451
+ `fixture_resolution.products.${handle}.match`
452
+ );
263
453
  specs.push({
264
454
  entityType: "product",
265
455
  handle,
266
456
  fixture: requirementFixture,
267
457
  strategies: [...declaration?.strategies ?? ["seed"]],
268
- clauses: normalizeFixtureMatchExpression(declaration?.match, `fixture_resolution.products.${handle}.match`),
269
- allowReuse: declaration?.allow_reuse === true
458
+ clauses: productClauses,
459
+ allowReuse: declaration?.allow_reuse === true,
460
+ contextOutputs: normalizeFixtureContextOutputs(
461
+ declaration?.context_outputs,
462
+ `fixture_resolution.products.${handle}.context_outputs`,
463
+ productClauses
464
+ )
270
465
  });
271
466
  }
272
467
  const declaredPricing = storyboard.fixture_resolution?.pricing_options ?? [];
@@ -288,17 +483,24 @@ function buildFixtureResolutionSpecs(storyboard) {
288
483
  const handle = fixture.pricing_option_id;
289
484
  if (typeof parent !== "string" || !parent || typeof handle !== "string" || !handle) continue;
290
485
  const { product_id: _parent, pricing_option_id: _handle, ...requirementFixture } = fixture;
486
+ const pricingClauses = normalizeFixtureMatchExpression(
487
+ declaration?.match,
488
+ `fixture_resolution.pricing_options.${parent}/${handle}.match`
489
+ );
291
490
  specs.push({
292
491
  entityType: "product_pricing_option",
293
492
  handle,
294
493
  parentProductHandle: parent,
295
494
  fixture: requirementFixture,
296
495
  strategies: [...declaration?.strategies ?? ["seed"]],
297
- clauses: normalizeFixtureMatchExpression(
298
- declaration?.match,
299
- `fixture_resolution.pricing_options.${parent}/${handle}.match`
300
- ),
301
- allowReuse: declaration?.allow_reuse === true
496
+ clauses: pricingClauses,
497
+ allowReuse: declaration?.allow_reuse === true,
498
+ contextOutputs: normalizeFixtureContextOutputs(
499
+ declaration?.context_outputs,
500
+ `fixture_resolution.pricing_options.${parent}/${handle}.context_outputs`,
501
+ pricingClauses,
502
+ "pricing_option"
503
+ )
302
504
  });
303
505
  }
304
506
  return specs;
@@ -417,7 +619,9 @@ function applyFixtureBindingsToRequest(request, toolName, bindings, version = im
417
619
  FixtureBindingRegistry,
418
620
  applyFixtureBindingsToRequest,
419
621
  buildFixtureResolutionSpecs,
622
+ matchFixtureRequirementsWithWitness,
420
623
  matchesFixtureRequirements,
624
+ normalizeFixtureContextOutputs,
421
625
  normalizeFixtureMatchExpression,
422
626
  validateFixtureResolutionDeclarations
423
627
  });
@@ -2,8 +2,22 @@ import { isDeepStrictEqual } from "node:util";
2
2
  import { ADCP_VERSION } from "../../version.mjs";
3
3
  import { getRequestSchemaEntityPaths } from "../../validation/schema-loader.mjs";
4
4
  import { productCanonicalFormatSatisfies } from "./canonical-format-satisfaction.mjs";
5
- const OPERATORS = ["equals", "present", "contains_all", "any_match", "canonical_format_satisfies"];
6
- const DECLARATION_KEYS = /* @__PURE__ */ new Set(["handle", "strategies", "match", "allow_reuse"]);
5
+ import {
6
+ COLLECTION_WITNESS_KINDS,
7
+ evaluateCollectionSelection,
8
+ normalizeCollectionSelectionSelector
9
+ } from "./collection-selection.mjs";
10
+ import { parsePath } from "./path.mjs";
11
+ import { assertWellFormedProtectedKey, isProtectedContextKey } from "./protected-context.mjs";
12
+ const OPERATORS = [
13
+ "equals",
14
+ "present",
15
+ "contains_all",
16
+ "any_match",
17
+ "canonical_format_satisfies",
18
+ "collection_selection_satisfies"
19
+ ];
20
+ const DECLARATION_KEYS = /* @__PURE__ */ new Set(["handle", "strategies", "match", "allow_reuse", "context_outputs"]);
7
21
  const PRICING_DECLARATION_KEYS = /* @__PURE__ */ new Set([...DECLARATION_KEYS, "product_handle"]);
8
22
  function isRecord(value) {
9
23
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -75,6 +89,12 @@ function normalizeClause(value, location) {
75
89
  operator,
76
90
  where: normalizeFixtureMatchExpression(value.where, `${location}.where`)
77
91
  };
92
+ case "collection_selection_satisfies":
93
+ return {
94
+ path: value.path,
95
+ operator,
96
+ value: normalizeCollectionSelectionSelector(value.value, `${location}.value`)
97
+ };
78
98
  case "canonical_format_satisfies":
79
99
  assertPlainObject(value.value, `${location}.value`);
80
100
  if (typeof value.value.format_kind !== "string" || value.value.format_kind.length === 0) {
@@ -86,7 +106,7 @@ function normalizeClause(value, location) {
86
106
  return { path: value.path, operator, value: value.value };
87
107
  }
88
108
  }
89
- function validateDeclaration(declaration, location, allowedKeys) {
109
+ function validateDeclaration(declaration, location, allowedKeys, entityKind = "product") {
90
110
  assertPlainObject(declaration, location);
91
111
  const unknown = Object.keys(declaration).filter((key) => !allowedKeys.has(key));
92
112
  if (unknown.length > 0) throw new Error(`${location}: unknown key(s): ${unknown.join(", ")}`);
@@ -114,10 +134,87 @@ function validateDeclaration(declaration, location, allowedKeys) {
114
134
  if (Array.isArray(declaration.strategies) && declaration.strategies.includes("discover") && clauses.length === 0) {
115
135
  throw new Error(`${location}.match: discover strategy requires at least one authored predicate`);
116
136
  }
137
+ normalizeFixtureContextOutputs(declaration.context_outputs, `${location}.context_outputs`, clauses, entityKind);
138
+ }
139
+ const FIXTURE_CONTEXT_OUTPUT_KEYS = /* @__PURE__ */ new Set(["key", "path", "witness", "snapshot"]);
140
+ function normalizeFixtureContextOutputs(value, location, clauses, entityKind = "product") {
141
+ if (value === void 0) return [];
142
+ assertArray(value, location);
143
+ if (value.length === 0) throw new Error(`${location}: must contain at least one capture when present`);
144
+ const hasCollectionPredicate = clauses.some((clause) => clause.operator === "collection_selection_satisfies");
145
+ const seen = /* @__PURE__ */ new Set();
146
+ return value.map((raw, index) => {
147
+ const entryLocation = `${location}[${index}]`;
148
+ assertPlainObject(raw, entryLocation);
149
+ const unknown = Object.keys(raw).filter((key) => !FIXTURE_CONTEXT_OUTPUT_KEYS.has(key));
150
+ if (unknown.length > 0) {
151
+ throw new Error(`${entryLocation}: unknown key(s): ${unknown.join(", ")}`);
152
+ }
153
+ if (typeof raw.key !== "string" || raw.key.length === 0) {
154
+ throw new Error(`${entryLocation}.key: must be a non-empty string`);
155
+ }
156
+ if (!isProtectedContextKey(raw.key)) {
157
+ throw new Error(
158
+ `${entryLocation}.key: fixture captures must use the reserved "__adcp_fixture_" prefix so later steps, caller context and seller responses cannot replace them; received "${raw.key}"`
159
+ );
160
+ }
161
+ assertWellFormedProtectedKey(raw.key, entryLocation + ".key");
162
+ if (seen.has(raw.key)) throw new Error(`${entryLocation}.key: duplicate capture key "${raw.key}"`);
163
+ seen.add(raw.key);
164
+ const forms = ["path", "witness", "snapshot"].filter((form) => Object.hasOwn(raw, form));
165
+ if (forms.length !== 1) {
166
+ throw new Error(
167
+ `${entryLocation}: must set exactly one of 'path', 'witness' or 'snapshot'; received ${forms.length === 0 ? "none" : forms.join(" + ")}`
168
+ );
169
+ }
170
+ const hasPath = forms[0] === "path";
171
+ if (forms[0] === "snapshot") {
172
+ if (raw.snapshot !== "selected_entity") {
173
+ throw new Error(`${entryLocation}.snapshot: must be 'selected_entity'`);
174
+ }
175
+ return { key: raw.key, snapshot: "selected_entity" };
176
+ }
177
+ if (hasPath) {
178
+ if (typeof raw.path !== "string" || raw.path.length === 0) {
179
+ throw new Error(`${entryLocation}.path: must be a non-empty string`);
180
+ }
181
+ if (raw.path.startsWith("/")) {
182
+ throw new Error(
183
+ `${entryLocation}.path: capture paths use the dotted storyboard grammar ("currency", "pricing_options[0].rate"), not the RFC 6901 pointers that match[].path takes; drop the leading "/" \u2014 received "${raw.path}"`
184
+ );
185
+ }
186
+ if (parsePath(raw.path).length === 0) {
187
+ throw new Error(
188
+ `${entryLocation}.path: must name at least one field in the dotted storyboard grammar ("currency", "pricing_options[0].rate") \u2014 received "${raw.path}", which resolves to the whole selected entity. To capture the whole entity, say so: snapshot: 'selected_entity'.`
189
+ );
190
+ }
191
+ if (raw.path.split(".").some((segment) => segment.length === 0)) {
192
+ throw new Error(`${entryLocation}.path: every dotted segment must name a field \u2014 received "${raw.path}".`);
193
+ }
194
+ return { key: raw.key, path: raw.path };
195
+ }
196
+ if (typeof raw.witness !== "string" || !COLLECTION_WITNESS_KINDS.includes(raw.witness)) {
197
+ throw new Error(`${entryLocation}.witness: must be one of ${COLLECTION_WITNESS_KINDS.join(", ")}`);
198
+ }
199
+ if (entityKind === "pricing_option") {
200
+ throw new Error(
201
+ `${entryLocation}.witness: a collection witness describes the product, not one of its pricing options; declare it under fixture_resolution.products.<product_handle>.context_outputs instead`
202
+ );
203
+ }
204
+ if (!hasCollectionPredicate) {
205
+ throw new Error(
206
+ `${entryLocation}.witness: requires a collection_selection_satisfies predicate on the same declaration`
207
+ );
208
+ }
209
+ return { key: raw.key, witness: raw.witness };
210
+ });
117
211
  }
118
212
  function validateFixtureResolutionDeclarations(storyboard) {
119
213
  const root = storyboard.fixture_resolution;
120
- if (root === void 0) return;
214
+ if (root === void 0) {
215
+ assertNoReservedOrdinaryKeys(storyboard);
216
+ return;
217
+ }
121
218
  assertPlainObject(root, `[${storyboard.id}] fixture_resolution`);
122
219
  const unknownRoot = Object.keys(root).filter((key) => key !== "products" && key !== "pricing_options");
123
220
  if (unknownRoot.length > 0) {
@@ -167,7 +264,7 @@ function validateFixtureResolutionDeclarations(storyboard) {
167
264
  for (let index = 0; index < root.pricing_options.length; index++) {
168
265
  const declaration = root.pricing_options[index];
169
266
  const location = `[${storyboard.id}] fixture_resolution.pricing_options[${index}]`;
170
- validateDeclaration(declaration, location, PRICING_DECLARATION_KEYS);
267
+ validateDeclaration(declaration, location, PRICING_DECLARATION_KEYS, "pricing_option");
171
268
  const typedDeclaration = declaration;
172
269
  if (typeof typedDeclaration.product_handle !== "string" || typedDeclaration.product_handle.length === 0) {
173
270
  throw new Error(`${location}.product_handle: must be a non-empty string`);
@@ -184,6 +281,68 @@ function validateFixtureResolutionDeclarations(storyboard) {
184
281
  }
185
282
  }
186
283
  }
284
+ assertNoReservedOrdinaryKeys(storyboard);
285
+ assertUniqueFixtureCaptureKeys(storyboard);
286
+ }
287
+ function assertNoReservedOrdinaryKeys(storyboard) {
288
+ const reject = (key, location) => {
289
+ if (isProtectedContextKey(key)) {
290
+ throw new Error(
291
+ `${location}: "${key}" uses the reserved "__adcp_fixture_" prefix, which is owned by fixture preparation. Rename it to any key not starting with "__adcp_fixture_".`
292
+ );
293
+ }
294
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
295
+ throw new Error(
296
+ `${location}: "${key}" is a prototype-sensitive context key and cannot be used; rename it to anything other than __proto__, constructor or prototype`
297
+ );
298
+ }
299
+ };
300
+ for (const key of Object.keys(storyboard.context ?? {})) {
301
+ reject(key, `[${storyboard.id}] context`);
302
+ }
303
+ for (const phase of storyboard.phases ?? []) {
304
+ for (const step of phase.steps ?? []) {
305
+ for (const output of step.context_outputs ?? []) {
306
+ if (typeof output.key !== "string") continue;
307
+ if (isProtectedContextKey(output.key)) {
308
+ throw new Error(
309
+ `[${storyboard.id}] phase '${phase.id}' step '${step.id}': context_outputs entry "${output.key}" uses the reserved "__adcp_fixture_" prefix; step captures cannot write fixture-preparation state. Declare it under fixture_resolution instead, or rename the key.`
310
+ );
311
+ }
312
+ reject(output.key, `[${storyboard.id}] phase '${phase.id}' step '${step.id}' context_outputs`);
313
+ }
314
+ const gates = step.not_applicable_if;
315
+ for (const gate of gates === void 0 ? [] : Array.isArray(gates) ? gates : [gates]) {
316
+ for (const key of gate.context_keys ?? []) {
317
+ if (typeof key === "string") {
318
+ reject(key, `[${storyboard.id}] phase '${phase.id}' step '${step.id}' not_applicable_if.context_keys`);
319
+ }
320
+ }
321
+ }
322
+ }
323
+ }
324
+ }
325
+ function assertUniqueFixtureCaptureKeys(storyboard) {
326
+ const owners = /* @__PURE__ */ new Map();
327
+ const claim = (key, owner) => {
328
+ const existing = owners.get(key);
329
+ if (existing !== void 0) {
330
+ throw new Error(
331
+ `[${storyboard.id}] fixture_resolution: capture key "${key}" is declared by both ${existing} and ${owner}`
332
+ );
333
+ }
334
+ owners.set(key, owner);
335
+ };
336
+ for (const declaration of storyboard.fixture_resolution?.products ?? []) {
337
+ for (const output of declaration.context_outputs ?? []) {
338
+ claim(output.key, `products."${declaration.handle}"`);
339
+ }
340
+ }
341
+ for (const declaration of storyboard.fixture_resolution?.pricing_options ?? []) {
342
+ for (const output of declaration.context_outputs ?? []) {
343
+ claim(output.key, `pricing_options."${declaration.product_handle}/${declaration.handle}"`);
344
+ }
345
+ }
187
346
  }
188
347
  function resolveCandidatePath(candidate, pointer) {
189
348
  let current = candidate;
@@ -207,9 +366,38 @@ function matchesFixtureRequirements(candidate, clauses) {
207
366
  return Array.isArray(actual) && actual.some((value) => matchesFixtureRequirements(value, clause.where ?? []));
208
367
  case "canonical_format_satisfies":
209
368
  return productCanonicalFormatSatisfies(actual, clause.value);
369
+ case "collection_selection_satisfies":
370
+ return evaluateCollectionSelection(actual, clause.value).eligible;
210
371
  }
211
372
  });
212
373
  }
374
+ function matchFixtureRequirementsWithWitness(candidate, clauses) {
375
+ let witness;
376
+ let matched = true;
377
+ let reason;
378
+ for (const clause of clauses) {
379
+ if (clause.operator !== "collection_selection_satisfies") {
380
+ if (matched && !matchesFixtureRequirements(candidate, [clause])) {
381
+ matched = false;
382
+ reason = `${clause.operator} at "${clause.path || "(whole candidate)"}" did not match`;
383
+ }
384
+ continue;
385
+ }
386
+ const actual = resolveCandidatePath(candidate, clause.path);
387
+ const evaluation = evaluateCollectionSelection(actual, clause.value);
388
+ if (!evaluation.eligible) {
389
+ if (matched) reason = evaluation.reason;
390
+ matched = false;
391
+ continue;
392
+ }
393
+ witness = evaluation.witness;
394
+ }
395
+ return {
396
+ matched,
397
+ ...witness && { witness },
398
+ ...reason !== void 0 && { reason }
399
+ };
400
+ }
213
401
  function buildFixtureResolutionSpecs(storyboard) {
214
402
  const specs = [];
215
403
  const productFixtures = new Map(
@@ -232,13 +420,22 @@ function buildFixtureResolutionSpecs(storyboard) {
232
420
  if (!fixture) continue;
233
421
  if (typeof fixture.product_id !== "string" || fixture.product_id.length === 0) continue;
234
422
  const { product_id: handle, ...requirementFixture } = fixture;
423
+ const productClauses = normalizeFixtureMatchExpression(
424
+ declaration?.match,
425
+ `fixture_resolution.products.${handle}.match`
426
+ );
235
427
  specs.push({
236
428
  entityType: "product",
237
429
  handle,
238
430
  fixture: requirementFixture,
239
431
  strategies: [...declaration?.strategies ?? ["seed"]],
240
- clauses: normalizeFixtureMatchExpression(declaration?.match, `fixture_resolution.products.${handle}.match`),
241
- allowReuse: declaration?.allow_reuse === true
432
+ clauses: productClauses,
433
+ allowReuse: declaration?.allow_reuse === true,
434
+ contextOutputs: normalizeFixtureContextOutputs(
435
+ declaration?.context_outputs,
436
+ `fixture_resolution.products.${handle}.context_outputs`,
437
+ productClauses
438
+ )
242
439
  });
243
440
  }
244
441
  const declaredPricing = storyboard.fixture_resolution?.pricing_options ?? [];
@@ -260,17 +457,24 @@ function buildFixtureResolutionSpecs(storyboard) {
260
457
  const handle = fixture.pricing_option_id;
261
458
  if (typeof parent !== "string" || !parent || typeof handle !== "string" || !handle) continue;
262
459
  const { product_id: _parent, pricing_option_id: _handle, ...requirementFixture } = fixture;
460
+ const pricingClauses = normalizeFixtureMatchExpression(
461
+ declaration?.match,
462
+ `fixture_resolution.pricing_options.${parent}/${handle}.match`
463
+ );
263
464
  specs.push({
264
465
  entityType: "product_pricing_option",
265
466
  handle,
266
467
  parentProductHandle: parent,
267
468
  fixture: requirementFixture,
268
469
  strategies: [...declaration?.strategies ?? ["seed"]],
269
- clauses: normalizeFixtureMatchExpression(
270
- declaration?.match,
271
- `fixture_resolution.pricing_options.${parent}/${handle}.match`
272
- ),
273
- allowReuse: declaration?.allow_reuse === true
470
+ clauses: pricingClauses,
471
+ allowReuse: declaration?.allow_reuse === true,
472
+ contextOutputs: normalizeFixtureContextOutputs(
473
+ declaration?.context_outputs,
474
+ `fixture_resolution.pricing_options.${parent}/${handle}.context_outputs`,
475
+ pricingClauses,
476
+ "pricing_option"
477
+ )
274
478
  });
275
479
  }
276
480
  return specs;
@@ -388,7 +592,9 @@ export {
388
592
  FixtureBindingRegistry,
389
593
  applyFixtureBindingsToRequest,
390
594
  buildFixtureResolutionSpecs,
595
+ matchFixtureRequirementsWithWitness,
391
596
  matchesFixtureRequirements,
597
+ normalizeFixtureContextOutputs,
392
598
  normalizeFixtureMatchExpression,
393
599
  validateFixtureResolutionDeclarations
394
600
  };
@@ -16,9 +16,16 @@ export type { CapturedWebhook, CapturedWebhookChallenge, CreateWebhookReceiverOp
16
16
  export { createRunnerVariables } from './context.mjs';
17
17
  export type { RunnerVariables } from './context.mjs';
18
18
  export { WEBHOOK_ASSERTION_TASKS } from './webhook-assertions.mjs';
19
- export { runStoryboard, runStoryboardStep, applyAdcpVersionRunOptions, applyStoryboardVersionOptions, getFirstStepPreview, summarizeStrictValidation, listStrictOnlyFailures, resolveCapabilityPath, evaluateCapabilityPredicate, describeStoryboardCapabilityGates, buildDiscoveryFailedResult, } from './runner.mjs';
19
+ export { prepareStoryboardFixtures, runStoryboard, runStoryboardStep, applyAdcpVersionRunOptions, applyStoryboardVersionOptions, getFirstStepPreview, summarizeStrictValidation, listStrictOnlyFailures, resolveCapabilityPath, evaluateCapabilityPredicate, describeStoryboardCapabilityGates, buildDiscoveryFailedResult, } from './runner.mjs';
20
20
  export { parseStoryboard, loadStoryboardFile, normalizeValidationOnlyTasks, VALIDATION_ONLY_TASK } from './loader.mjs';
21
- export { applyFixtureBindingsToRequest, buildFixtureResolutionSpecs, FixtureBindingRegistry, matchesFixtureRequirements, normalizeFixtureMatchExpression, validateFixtureResolutionDeclarations, } from './fixture-resolution.mjs';
21
+ export { applyFixtureBindingsToRequest, buildFixtureResolutionSpecs, FixtureBindingRegistry, matchesFixtureRequirements, matchFixtureRequirementsWithWitness, normalizeFixtureContextOutputs, normalizeFixtureMatchExpression, validateFixtureResolutionDeclarations, } from './fixture-resolution.mjs';
22
+ export type { FixtureContextOutput } from './types.mjs';
23
+ export { COLLECTION_DIAGNOSTIC_LIMIT, COLLECTION_PAIR_LIMIT, COLLECTION_TARGETING_MODES, COLLECTION_WITNESS_KINDS, compareCollectionPairs, diffCollectionPairSets, evaluateCollectionSelection, formatCollectionPair, normalizeCollectionSelectionSelector, normalizeCollectionSelectors, productSatisfiesCollectionSelection, readCollectionWitness, summarizeCollectionPairs, } from './collection-selection.mjs';
24
+ export type { ResolvedCollectionSelectionSelector, CollectionNormalizationCode, CollectionNormalizationFailure, CollectionNormalizationResult, CrossPublisherCollectionRelation, CollectionPair, CollectionSelectionEvaluation, CollectionSelectionSelector, CollectionSelectionWitness, CollectionSelectorObject, CollectionWitnessKind, CollectionWitnessReadResult, NormalizedCollectionSelectors, } from './collection-selection.mjs';
25
+ export { isProtectedContextKey, PROTECTED_CONTEXT_PREFIX, ProtectedContextViolation } from './protected-context.mjs';
26
+ export type { ProtectedContextViolationCode } from './protected-context.mjs';
27
+ export { isTrustedPreparedFixtures, PREPARED_FIXTURES_CONTRACT_VERSION, PreparedStoryboardFixtures, withPreparedFixtures, } from './prepared-fixtures.mjs';
28
+ export type { PreparedFixtureBinding, PreparedFixtureSelection, PreparedFixturesAuthorizer, PreparedFixturesDecision, PreparedFixturesDisposition, PreparedFixturesLookup, PreparedFixturesRefusal, PreparedFixturesRefusalCode, PreparedFixturesReference, PreparedSchemaArtifact, } from './prepared-fixtures.mjs';
22
29
  export { getComplianceCacheDir, getExternalSchemaRootForCompliance, loadComplianceIndex, listBundles, loadBundleStoryboards, listAllComplianceStoryboards, getComplianceStoryboardById, findBundleById, resolveBundleOrStoryboard, resolveStoryboardsForCapabilities, isComplianceVersionSupported, loadSpecialismDetail, listSpecialisms, CapabilityResolutionError, PROTOCOL_TO_PATH, UNBASELINED_SUPPORTED_PROTOCOLS, } from './compliance.mjs';
23
30
  export type { AgentCapabilities, BundleKind, BundleRef, CapabilityResolutionCode, ComplianceIndex, ComplianceIndexProtocol, ComplianceIndexSpecialism, NotApplicableStoryboard, ResolveOptions, ResolvedBundle, ResolvedStoryboards, SpecialismDetail, } from './compliance.mjs';
24
31
  export { TASK_TO_METHOD, executeStoryboardTask } from './task-map.mjs';