@volter/twin-standard 1.0.157 → 1.0.159

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 (76) hide show
  1. package/README.md +3 -2
  2. package/client-sdks/import.mjs +2 -0
  3. package/client-sdks/package.json +4 -0
  4. package/client-sdks/pnpm-lock.yaml +609 -0
  5. package/dist/client-sdks/import.mjs +2 -0
  6. package/dist/client-sdks/package.json +4 -0
  7. package/dist/client-sdks/pnpm-lock.yaml +609 -0
  8. package/dist/src/assessment.d.ts +13 -3
  9. package/dist/src/assessment.js +10 -6
  10. package/dist/src/browser-assessment.js +16 -13
  11. package/dist/src/clients/harness.d.ts +25 -4
  12. package/dist/src/clients/harness.js +184 -22
  13. package/dist/src/clients/jira.js +11 -2
  14. package/dist/src/clients/run.js +19 -14
  15. package/dist/src/clients/tinybird.js +15 -6
  16. package/dist/src/clients/vercel.d.ts +2 -0
  17. package/dist/src/clients/vercel.js +73 -0
  18. package/dist/src/conformance.d.ts +2 -0
  19. package/dist/src/conformance.js +392 -184
  20. package/dist/src/consumer.d.ts +22 -1
  21. package/dist/src/consumer.js +91 -26
  22. package/dist/src/credential-issuer-import.d.ts +3 -0
  23. package/dist/src/credential-issuer-import.js +42 -0
  24. package/dist/src/derive.js +3 -0
  25. package/dist/src/gate.js +4 -0
  26. package/dist/src/lanes.d.ts +2 -0
  27. package/dist/src/lanes.js +7 -0
  28. package/dist/src/protocol-3.js +8 -3
  29. package/dist/src/spec-documents.d.ts +4 -0
  30. package/dist/src/spec-documents.js +110 -3
  31. package/dist/src/tools/life-coverage-walk.js +38 -26
  32. package/dist/src/tools/score-pack.js +6 -5
  33. package/dist/src/tools/vendor-examples.js +91 -80
  34. package/dist/src/walk/behavior-journey.d.ts +22 -14
  35. package/dist/src/walk/behavior-journey.js +182 -133
  36. package/dist/src/walk/journey-kit.d.ts +3 -2
  37. package/dist/src/walk/journey-kit.js +6 -2
  38. package/dist/src/walk/replay.d.ts +23 -0
  39. package/dist/src/walk/replay.js +118 -0
  40. package/dist/src/walk/serve-pack.d.ts +2 -2
  41. package/dist/src/walk/serve-pack.js +38 -2
  42. package/dist/src/walk/shape.d.ts +25 -4
  43. package/dist/src/walk/shape.js +166 -56
  44. package/dist/src/walk/vendor-fetch.d.ts +7 -2
  45. package/dist/src/walk/vendor-fetch.js +13 -2
  46. package/dist/src/walk/walk-database.d.ts +9 -1
  47. package/dist/src/walk/walk-database.js +35 -13
  48. package/dist/src/walk/xml-view.d.ts +3 -2
  49. package/dist/src/walk/xml-view.js +5 -4
  50. package/package.json +4 -3
  51. package/src/assessment.ts +12 -8
  52. package/src/browser-assessment.ts +12 -5
  53. package/src/clients/harness.ts +142 -19
  54. package/src/clients/jira.ts +4 -2
  55. package/src/clients/run.ts +17 -15
  56. package/src/clients/tinybird.ts +8 -6
  57. package/src/clients/vercel.ts +67 -0
  58. package/src/conformance.ts +313 -128
  59. package/src/consumer.ts +83 -21
  60. package/src/credential-issuer-import.ts +38 -0
  61. package/src/derive.ts +3 -1
  62. package/src/gate.ts +3 -0
  63. package/src/lanes.ts +7 -0
  64. package/src/protocol-3.ts +7 -3
  65. package/src/spec-documents.ts +92 -3
  66. package/src/tools/life-coverage-walk.ts +32 -22
  67. package/src/tools/score-pack.ts +7 -6
  68. package/src/tools/vendor-examples.ts +88 -75
  69. package/src/walk/behavior-journey.ts +145 -109
  70. package/src/walk/journey-kit.ts +7 -5
  71. package/src/walk/replay.ts +103 -0
  72. package/src/walk/serve-pack.ts +25 -5
  73. package/src/walk/shape.ts +156 -54
  74. package/src/walk/vendor-fetch.ts +16 -4
  75. package/src/walk/walk-database.ts +41 -15
  76. package/src/walk/xml-view.ts +5 -4
@@ -16,7 +16,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
16
16
  import { join } from 'node:path';
17
17
  import { listActions, observeTransitions, stateOf } from '@volter/world-core/runtime';
18
18
  import { inMemoryWorld, walk, usePacks } from "../walk/behavior-journey.js";
19
- import { lanesOf, shapeChecker } from "../walk/shape.js";
19
+ import { lanesOf, lineShape, shapeChecker } from "../walk/shape.js";
20
20
  import { sharingLanesIn } from "../lanes.js";
21
21
  /** Registers, in the `bun test` run that imports it, the walks of `vendor` in the pack repository at `PACKS`. */
22
22
  export async function coverageWalks(PACKS, vendor) {
@@ -36,9 +36,12 @@ export async function coverageWalks(PACKS, vendor) {
36
36
  const { manifest } = (await import(__rewriteRelativeImportExtension(join(PACKS, vendor, 'src', 'manifest.ts'))));
37
37
  // the lanes over the vendor's state its serve factory serves are walked by this life, their machines with the vendor's
38
38
  // ("Pack layout"); two manifests naming one resource's field would collide in the keys below, so that refuses
39
- const laneManifests = await Promise.all(sharingLanes(vendor).map(async (l) => (await import(__rewriteRelativeImportExtension(join(PACKS, vendor, l, 'src', 'manifest.ts')))).manifest));
39
+ const units = [{ dir: join(PACKS, vendor), manifest, lane: undefined }, ...await Promise.all(sharingLanes(vendor).map(async (l) => {
40
+ const dir = join(PACKS, vendor, l);
41
+ return { dir, lane: l, manifest: (await import(__rewriteRelativeImportExtension(join(dir, 'src', 'manifest.ts')))).manifest };
42
+ }))];
40
43
  // a vendor of lanes' root (a VendorManifest) declares no resources of its own: its lanes do
41
- const allResources = [manifest, ...laneManifests].flatMap((mf) => Object.entries(mf.resources ?? {}));
44
+ const allResources = units.flatMap(({ manifest: mf }) => Object.entries(mf.resources ?? {}));
42
45
  const machines = new Map();
43
46
  for (const [resource, decl] of allResources)
44
47
  for (const [field, machine] of Object.entries(decl.state ?? {})) {
@@ -50,23 +53,28 @@ export async function coverageWalks(PACKS, vendor) {
50
53
  machines.set(name, machine);
51
54
  }
52
55
  const where = new Map();
53
- for (const mf of [manifest, ...laneManifests])
56
+ const httpTransitions = new Set();
57
+ for (const { dir, manifest: mf } of units) {
58
+ const http = !lineShape(dir);
54
59
  for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
55
60
  for (const [field, m] of Object.entries(decl.state ?? {}))
56
61
  m.transitions.forEach((t, i) => {
62
+ if (http)
63
+ httpTransitions.add(t);
57
64
  const place = { at: `${resource}.${field}#${i}`, field, type: decl.storedAs ?? resource, service: mf.service ?? mf.vendor };
58
65
  const places = where.get(t) ?? [];
59
66
  if (!places.some((p) => p.at === place.at && p.type === place.type && p.service === place.service))
60
67
  where.set(t, [...places, place]);
61
68
  });
62
69
  }
70
+ }
63
71
  const moves = new Map();
64
72
  const subjectKey = (service, type, id) => JSON.stringify([service, type, id]);
65
73
  observeTransitions((_decl, t, outcome, from, entry) => {
66
74
  const places = where.get(t) ?? [];
67
75
  if (outcome === 'refused') {
68
- // A failed check on an already committed write is no refusal the application received.
69
- if (entry)
76
+ // HTTP refusals are credited from their received status and body; a line protocol owns its session's replies.
77
+ if (entry || httpTransitions.has(t))
70
78
  return;
71
79
  const groups = new Set(places.map((p) => JSON.stringify([p.service, p.type, p.field])));
72
80
  if (groups.size === 1)
@@ -100,11 +108,14 @@ export async function coverageWalks(PACKS, vendor) {
100
108
  const shape = await shapeChecker(join(PACKS, vendor));
101
109
  const answered = [];
102
110
  try {
103
- await walk(life, root, failures, { beats: w.beats, hooks: { after: (step, path, answer) => {
111
+ await walk(life, root, failures, { beats: w.beats, hooks: { after: (_step, _path, answer) => {
104
112
  // a line step's refusals are the machine's own (observeTransitions); a request's are read off its answer
105
- const op = 'method' in step ? shape?.operationOf(step.method, path, { ...(life.headers ?? {}), ...(step.headers ?? {}) })?.op.id : undefined;
106
- if (op)
107
- answered.push({ op, text: JSON.stringify(answer.body ?? ''), body: answer.body });
113
+ const request = answer.request;
114
+ if (!request)
115
+ return;
116
+ const hit = shape?.operationOf(request.method, request.url, request.headers);
117
+ if (hit)
118
+ answered.push({ op: hit.op.id, lane: hit.lane, status: answer.status, body: answer.wireBody });
108
119
  } } });
109
120
  // a declared refusal is reached when a step on its operation was answered with it: its code, or its message
110
121
  // up to the first placeholder
@@ -112,26 +123,27 @@ export async function coverageWalks(PACKS, vendor) {
112
123
  const holds = (value, code) => (value !== null && typeof value === 'object'
113
124
  ? Object.values(value).some((v) => holds(v, code))
114
125
  : (typeof value === 'string' || typeof value === 'number') && String(value) === code);
115
- const answers = (a, r) => r.code !== undefined ? holds(a.body, String(r.code)) : a.text.includes(r.message.split('{')[0].trim());
116
- for (const [resource, decl] of allResources) {
117
- for (const [field, m] of Object.entries(decl.state ?? {})) {
118
- m.transitions.forEach((t, i) => {
119
- if (!t.operation)
120
- return;
121
- const on = answered.filter((a) => a.op === t.operation);
122
- if (t.refusal && on.some((a) => answers(a, t.refusal)))
123
- seen.add(`${resource}.${field}#${i}|refused|*`);
124
- for (const [from, r] of Object.entries(t.refusals ?? {}))
125
- if (on.some((a) => answers(a, r)))
126
- seen.add(`${resource}.${field}#${i}|refused|${from}`);
127
- });
126
+ const answers = (a, r) => a.status === r.status && (r.code !== undefined ? holds(a.body, String(r.code)) : JSON.stringify(a.body)?.includes(r.message.split('{')[0].trim()) === true);
127
+ for (const { manifest: mf, lane } of units)
128
+ for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
129
+ for (const [field, m] of Object.entries(decl.state ?? {})) {
130
+ m.transitions.forEach((t, i) => {
131
+ if (!t.operation)
132
+ return;
133
+ const on = answered.filter((a) => a.op === t.operation && a.lane === lane);
134
+ if (t.refusal && on.some((a) => answers(a, t.refusal)))
135
+ seen.add(`${resource}.${field}#${i}|refused|*`);
136
+ for (const [from, r] of Object.entries(t.refusals ?? {}))
137
+ if (on.some((a) => answers(a, r)))
138
+ seen.add(`${resource}.${field}#${i}|refused|${from}`);
139
+ });
140
+ }
128
141
  }
129
- }
130
142
  // every change the life made to a declared state field, from the World's log in order: a move is the declared
131
143
  // transition that allows it (from its state to the next, by its operation when both name one); a change no
132
144
  // transition allows is an undeclared move
133
145
  const fieldsOf = new Map();
134
- for (const mf of [manifest, ...laneManifests])
146
+ for (const { manifest: mf } of units)
135
147
  for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
136
148
  const key = JSON.stringify([mf.service ?? mf.vendor, decl.storedAs ?? resource]);
137
149
  for (const [field, m] of Object.entries(decl.state ?? {}))
@@ -139,7 +151,7 @@ export async function coverageWalks(PACKS, vendor) {
139
151
  }
140
152
  const current = new Map();
141
153
  // the services the life wrote: the vendor's, or its lanes' (a vendor of lanes' root names none of its own)
142
- const services = [...new Set([manifest, ...laneManifests].map((mf) => mf.service).filter((x) => typeof x === 'string'))];
154
+ const services = [...new Set(units.map(({ manifest: mf }) => mf.service).filter((x) => typeof x === 'string'))];
143
155
  if (!services.length)
144
156
  services.push(manifest.vendor);
145
157
  for (const a of services.flatMap((service) => listActions(service, root))) {
@@ -3,7 +3,7 @@
3
3
  // life per pack is walked through the pack's own fetch on the in-memory World, and the report says:
4
4
  // - the life: whether a judge's plausible verdict names its current content (a verdict its author wrote is not one);
5
5
  // - correctness: every check on every step (status, expectations, captures, SHAPE, the beats) and the life
6
- // answering byte for byte the same on a second fresh World;
6
+ // answering the same on a second fresh World under its declared engine outputs;
7
7
  // - coverage: the hand-written code the life and the vendor's published examples run and the declared state logic
8
8
  // they reach (life-coverage.ts), with what they never reach;
9
9
  // - the vendor's published examples: replayed and judged (vendor-examples.ts);
@@ -26,6 +26,7 @@ import { dirname, join, sep } from 'node:path';
26
26
  import { inMemoryWorld, walk, withPinnedWorldDraws, usePacks } from "../walk/behavior-journey.js";
27
27
  import { expandFills, waitMs } from "../walk/journey-kit.js";
28
28
  import { vendorFetch, vendorStream } from "../walk/vendor-fetch.js";
29
+ import { compareReplays } from "../walk/replay.js";
29
30
  import { existenceProbe } from "../walk/journey-kit.js";
30
31
  import { kernelCheckout, packsRoot, toolPath } from "./packs-root.js";
31
32
  const PACKS = packsRoot();
@@ -85,7 +86,7 @@ await walk(life, world.root(), failures, { beats: true });
85
86
  const answers = async () => {
86
87
  const seen = [];
87
88
  // the World's own draws (its seed, its keys) are the same in both: two Worlds differ only by what is beyond them (R9)
88
- await withPinnedWorldDraws(() => walk(life, world.root(), [], { beats: false, hooks: { after: (step, p, a) => seen.push(`${step.name ?? ''}\u0000${p} ${a.status} ${JSON.stringify(a.body)}`) } }));
89
+ await withPinnedWorldDraws(() => walk(life, world.root(), failures, { beats: false, hooks: { after: (step, path, answer) => seen.push({ name: step.name ?? '', path, status: answer.status, body: answer.body, ...(answer.sql ? { sql: answer.sql } : {}), ...(answer.connection ? { connection: answer.connection } : {}) }) } }));
89
90
  return seen;
90
91
  };
91
92
  const [first, second] = [await answers(), await answers()];
@@ -184,9 +185,9 @@ const flags = [];
184
185
  }
185
186
  }
186
187
  }
187
- const differs = first.findIndex((line, i) => line !== second[i]);
188
- if (differs >= 0 || first.length !== second.length)
189
- failures.push({ step: '(whole life)', beat: 'REPLAYABLE', detail: `a second fresh World answered differently at "${(first[differs] ?? second[differs] ?? first.at(-1) ?? '').split('\u0000')[0]}" (the ${differs + 1}th answered step; waits are not counted)` });
188
+ const replayComparison = compareReplays(first, second);
189
+ for (const detail of replayComparison.diagnostics)
190
+ failures.push({ step: '(whole life)', beat: 'REPLAYABLE', detail });
190
191
  // ── breadth ──────────────────────────────────────────────────────────────────────────────────
191
192
  const factory = await vendorFetch(PACKS, vendor);
192
193
  const fetch = factory?.(world.root());
@@ -56,7 +56,7 @@ import { basename, join } from 'node:path';
56
56
  import { gunzipSync } from 'node:zlib';
57
57
  import { inMemoryWorld, walk, usePacks } from "../walk/behavior-journey.js";
58
58
  import { vendorFetch, vendorStream } from "../walk/vendor-fetch.js";
59
- import { at as selectorAt, captureAt, expandFills, isResp, isGit, isLine, isSocket, isWait } from "../walk/journey-kit.js";
59
+ import { at as selectorAt, captureAt, expandFills, isResp, isGit, isSocket, isWait } from "../walk/journey-kit.js";
60
60
  import { lanesOf, shapeChecker } from "../walk/shape.js";
61
61
  import { applySpecPatch, discoveryToOpenAPI, mergeDocuments, readSpecDocuments, slug } from "../spec.js";
62
62
  import { packsRoot } from "./packs-root.js";
@@ -70,6 +70,7 @@ let specSchemas = {};
70
70
  const schemasByOperation = {};
71
71
  /** Each operation's request-body schema and its JSON answers' schemas by status, as the served spec gives them. */
72
72
  const operationSchemas = {};
73
+ const operationKey = (operation, lane) => lane ? `${lane}:${operation}` : operation;
73
74
  /** A page's placeholder id in an answer-map key is the id sent at the corresponding published request position.
74
75
  * Only declared placeholders bind, and an ambiguous repeated placeholder cannot stand for a guessed map key. */
75
76
  function answerKeys(output, published, sent, placeholder, rawBindings = false) {
@@ -106,7 +107,7 @@ function served(pkg, doc) {
106
107
  applySpecPatch(patched, p);
107
108
  return patched;
108
109
  }
109
- let xmlModel;
110
+ const restXmlService = (model) => Object.values(model.shapes).some((shape) => shape.type === 'service' && shape.traits?.['aws.protocols#restXml'] !== undefined);
110
111
  /** A translated placeholder-key map is a complete map even when its surrounding documented request is partial. */
111
112
  function requestMapKeysHold(published, translated, sent, placeholder) {
112
113
  if (!placeholder || !published || typeof published !== 'object')
@@ -124,11 +125,11 @@ function requestMapKeysHold(published, translated, sent, placeholder) {
124
125
  });
125
126
  }
126
127
  const PRELUDE = { Boolean: 'boolean', PrimitiveBoolean: 'boolean', Integer: 'integer', PrimitiveInteger: 'integer', Long: 'long', PrimitiveLong: 'long', Float: 'float', Double: 'double' };
127
- const scalarType = (target) => xmlModel?.shapes[target]?.type ?? PRELUDE[target.split('#')[1] ?? ''] ?? 'string';
128
- const scalarOf = (target, v) => {
128
+ const scalarType = (model, target) => model.shapes[target]?.type ?? PRELUDE[target.split('#')[1] ?? ''] ?? 'string';
129
+ const scalarOf = (model, target, v) => {
129
130
  if (typeof v !== 'string')
130
131
  return v;
131
- const t = scalarType(target);
132
+ const t = scalarType(model, target);
132
133
  if (t === 'boolean')
133
134
  return v === 'true';
134
135
  if (['integer', 'long', 'short', 'byte'].includes(t))
@@ -138,10 +139,10 @@ const scalarOf = (target, v) => {
138
139
  const arrayOf = (v) => (v === undefined || v === '' ? [] : Array.isArray(v) ? v : [v]);
139
140
  /** An answer's XML members (xml-view) read by the model's shape: each member under its xmlName, a flattened list the
140
141
  * repeated element, a wrapped list its container's member elements, scalars as their shape's type. */
141
- function typedXml(target, v) {
142
- const shape = xmlModel?.shapes[target];
142
+ function typedXml(model, target, v) {
143
+ const shape = model.shapes[target];
143
144
  if (!shape || shape.type !== 'structure')
144
- return scalarOf(target, v);
145
+ return scalarOf(model, target, v);
145
146
  if (!v || typeof v !== 'object')
146
147
  return v === '' ? {} : v;
147
148
  const obj = v;
@@ -150,38 +151,38 @@ function typedXml(target, v) {
150
151
  if (Object.keys(m.traits ?? {}).some((t) => /^smithy\.api#http(Header|PrefixHeaders|ResponseCode)$/.test(t)))
151
152
  continue;
152
153
  const el = String(m.traits?.['smithy.api#xmlName'] ?? name);
153
- const ts = xmlModel.shapes[m.target];
154
+ const ts = model.shapes[m.target];
154
155
  const raw = obj[el];
155
156
  if (ts?.type === 'list') {
156
157
  if (m.traits?.['smithy.api#xmlFlattened'] !== undefined) {
157
158
  if (raw !== undefined)
158
- out[name] = arrayOf(raw).map((i) => typedXml(ts.member.target, i));
159
+ out[name] = arrayOf(raw).map((i) => typedXml(model, ts.member.target, i));
159
160
  continue;
160
161
  }
161
162
  if (raw === undefined)
162
163
  continue;
163
164
  const memberEl = String(ts.member.traits?.['smithy.api#xmlName'] ?? 'member');
164
- out[name] = arrayOf(raw === '' ? undefined : raw[memberEl]).map((i) => typedXml(ts.member.target, i));
165
+ out[name] = arrayOf(raw === '' ? undefined : raw[memberEl]).map((i) => typedXml(model, ts.member.target, i));
165
166
  }
166
167
  else if (raw !== undefined)
167
- out[name] = typedXml(m.target, raw);
168
+ out[name] = typedXml(model, m.target, raw);
168
169
  }
169
170
  return out;
170
171
  }
171
172
  /** A restXml operation's success answer as its output structure: the body's members by shape (or its payload
172
173
  * member's), and the members its headers carry. */
173
- function restXmlAnswer(opId, body, headers = {}) {
174
- const out = xmlModel.shapes[xmlModel.shapes[opId]?.output?.target ?? ''];
174
+ function restXmlAnswer({ id: opId, model }, body, headers = {}) {
175
+ const out = model.shapes[model.shapes[opId]?.output?.target ?? ''];
175
176
  if (!out)
176
177
  return body;
177
178
  const members = Object.entries((out.members ?? {}));
178
179
  const payload = members.find(([, m]) => m.traits?.['smithy.api#httpPayload'] !== undefined);
179
- const answer = (payload ? { [payload[0]]: typedXml(payload[1].target, body) } : typedXml(xmlModel.shapes[opId].output.target, body));
180
+ const answer = (payload ? { [payload[0]]: typedXml(model, payload[1].target, body) } : typedXml(model, model.shapes[opId].output.target, body));
180
181
  const lower = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]));
181
182
  for (const [name, m] of members) {
182
183
  const h = m.traits?.['smithy.api#httpHeader'];
183
184
  if (h !== undefined && lower[String(h).toLowerCase()] !== undefined)
184
- answer[name] = scalarOf(m.target, lower[String(h).toLowerCase()]);
185
+ answer[name] = scalarOf(model, m.target, lower[String(h).toLowerCase()]);
185
186
  // a map bound to a header prefix (S3's Metadata, `x-amz-meta-*`): every header under it, by the rest of its name
186
187
  const prefix = m.traits?.['smithy.api#httpPrefixHeaders'];
187
188
  if (prefix !== undefined)
@@ -191,8 +192,8 @@ function restXmlAnswer(opId, body, headers = {}) {
191
192
  }
192
193
  /** Where a restXml example's input is not what the step sends: each member the example gives, at its binding (a path
193
194
  * label, a query key, a header; a payload or body member is the step's body and is not compared). */
194
- function restXmlInputProblems(opId, input, path, headers, placeholder, rawPath) {
195
- const shape = xmlModel.shapes[xmlModel.shapes[opId]?.input?.target ?? ''];
195
+ function restXmlInputProblems({ id: opId, model }, input, path, headers, placeholder, rawPath) {
196
+ const shape = model.shapes[model.shapes[opId]?.input?.target ?? ''];
196
197
  const url = new URL(path, 'http://twin.local');
197
198
  // the step as written, its captures unfilled: a value it takes from a capture is the World's own id (an S3 upload's
198
199
  // UploadId), which stands for the example's literal as a spec's ids stand for the World's (architecture, "Published examples")
@@ -236,11 +237,13 @@ function lenientJson(text) {
236
237
  * response's `example`/`examples` (a `$ref` to `components.examples` followed), a Swagger 2 response's `examples`, Stripe's
237
238
  * fixtures (one sample per resource, beside its spec), and OpenAI's `x-oaiMeta.examples` (its `response` is JSON text; one that is not JSON, a stream or an abridged answer, is
238
239
  * kept as text and compared with the published framing and payload). */
239
- function published(pkg) {
240
+ function published(pkg, lane) {
240
241
  // Both the vendor API and its lanes publish examples; a root spec must not hide a lane. A lane's example names its
241
242
  // lane, the operation being the lane's own (its owner is `<lane>:<id>` in the vendor's owners)
242
- const own = [...(hasOwnSurface(pkg) ? fromSpec(pkg) : []), ...fromDocs(pkg)];
243
- return [...own, ...lanesOf(pkg).flatMap((lane) => published(lane).map((e) => ({ ...e, lane: e.lane ? `${basename(lane)}:${e.lane}` : basename(lane) })))];
243
+ const smithy = ['smithy.json.gz', 'smithy.json'].map((name) => join(pkg, 'spec', name)).find(existsSync);
244
+ const model = smithy ? parseExactJson((smithy.endsWith('.gz') ? gunzipSync(readFileSync(smithy)) : readFileSync(smithy)).toString('utf8')) : undefined;
245
+ const own = [...(hasOwnSurface(pkg) ? fromSpec(pkg, lane, model) : []), ...fromDocs(pkg, lane, model)].map((e) => ({ ...e, lane }));
246
+ return [...own, ...lanesOf(pkg).flatMap((dir) => published(dir, lane ? `${lane}:${basename(dir)}` : basename(dir)))];
244
247
  }
245
248
  /** The examples a vendor's documentation pages publish beside its spec (a schedule page's expressions, each with what
246
249
  * it means), vendored in `spec/doc-examples.json` as `[{title, source, operation, request?, input?, output?, means, status?}]`:
@@ -251,7 +254,7 @@ function published(pkg) {
251
254
  * reference writes every property, and every scalar as a string, `"verified": "true"` beside a schema saying boolean)
252
255
  * shows what the answer can hold, not one request's answer: its example is a sample against the served schema, and a
253
256
  * string where the schema says number, integer or boolean is read as that type, in the answer and the request body. */
254
- function fromDocs(pkg) {
257
+ function fromDocs(pkg, lane, model) {
255
258
  const file = join(pkg, 'spec', 'doc-examples.json');
256
259
  if (!existsSync(file))
257
260
  return [];
@@ -294,12 +297,11 @@ function fromDocs(pkg) {
294
297
  for (const d of docs)
295
298
  if (d.placeholders && (!d.placeholders.names?.length || !d.placeholders.quote?.trim()))
296
299
  throw new Error(`${file}: "${d.title}" names placeholders without the page's words for them (placeholders.quote)`);
297
- const smithy = ['smithy.json.gz', 'smithy.json'].some((f) => existsSync(join(pkg, 'spec', f)));
298
300
  // a page's example is a fragment (an expression, not the request that carries it): the body holds it
299
301
  return docs.map((d) => {
300
302
  if (!d.rendered)
301
303
  return fromPage(d, d.output, d.input);
302
- const schemas = operationSchemas[d.operation];
304
+ const schemas = operationSchemas[operationKey(d.operation, lane)];
303
305
  const schema = schemas?.responses[String(d.status ?? 200)];
304
306
  if (!schema)
305
307
  throw new Error(`${file}: "${d.title}" is rendered from a schema the served spec does not give (${d.operation}, ${d.status ?? 200})`);
@@ -311,7 +313,14 @@ function fromDocs(pkg) {
311
313
  function fromPage(d, output, input) {
312
314
  const named = d.placeholders?.names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
313
315
  const placeholder = named ? new RegExp(`${placeholderIds.source}|^(?:${named})$`, placeholderIds.flags) : placeholderIds;
314
- return { names: d.placeholders?.names, ...(d.wire ? { wire: d.wire } : {}), id: `${d.operation}#doc ${d.title}`, operation: d.operation, output, ...(d.status ? { status: d.status } : {}), ...(input !== undefined ? { input, partial: true, placeholder } : {}), ...(smithy ? (xmlModel ? { restXml: Object.keys(xmlModel.shapes).find((k) => k.endsWith(`#${d.operation}`)) } : { target: d.operation }) : {}), ...(d.request ? { request: d.request } : {}), ...(authorizationAbsent.has(d) ? { authorizationAbsent: true } : {}) };
316
+ let restXml;
317
+ if (model && restXmlService(model)) {
318
+ const id = Object.keys(model.shapes).find((key) => key.endsWith(`#${d.operation}`));
319
+ if (!id)
320
+ throw new Error(`${file}: "${d.title}" names no operation in its XML service (${d.operation})`);
321
+ restXml = { id, model };
322
+ }
323
+ return { names: d.placeholders?.names, ...(d.wire ? { wire: d.wire } : {}), id: `${d.operation}#doc ${d.title}`, operation: d.operation, output, ...(d.status ? { status: d.status } : {}), ...(input !== undefined ? { input, partial: true, placeholder } : {}), ...(model ? (restXml ? { restXml } : { target: d.operation }) : {}), ...(d.request ? { request: d.request } : {}), ...(authorizationAbsent.has(d) ? { authorizationAbsent: true } : {}) };
315
324
  }
316
325
  }
317
326
  /** A rendered object the page does not fill: any object stands where it is. */
@@ -400,12 +409,25 @@ function decodedExample(media, value) {
400
409
  return value;
401
410
  }
402
411
  }
403
- function fromSpec(pkg) {
412
+ function fromSpec(pkg, lane, model) {
404
413
  const rfc = existsSync(join(pkg, 'spec')) ? readdirSync(join(pkg, 'spec')).find((f) => /^rfc\d+\.txt$/.test(f)) : undefined;
405
414
  if (rfc)
406
415
  return scenarios(readFileSync(join(pkg, 'spec', rfc), 'utf8'));
416
+ const out = [];
417
+ if (model) {
418
+ const restXml = restXmlService(model);
419
+ for (const [id, shape] of Object.entries(model.shapes)) {
420
+ const examples = shape.traits?.['smithy.api#examples'];
421
+ const operation = id.split('#')[1];
422
+ // an awsJson operation is named by its x-amz-target header, `<service>.<operation>`; its body is the input. A
423
+ // restXml operation (S3's) is named by its method and path, and its input is where each member's binding puts it
424
+ for (const e of examples ?? [])
425
+ out.push({ id: `${operation}#${e.title}`, operation, output: e.error ? e.error.content : e.output, input: e.input ?? {}, ...(restXml ? { restXml: { id, model } } : { target: operation }) });
426
+ }
427
+ return out;
428
+ }
407
429
  // a spec vendored as published or gzipped, as derive-pack reads it
408
- const spec = ['smithy.json.gz', 'smithy.json', 'openapi.json.gz', 'openapi.json', 'openapi.yaml.gz', 'openapi.yaml', 'discovery.json.gz', 'discovery.json'].map((f) => join(pkg, 'spec', f)).find((p) => existsSync(p));
430
+ const spec = ['openapi.json.gz', 'openapi.json', 'openapi.yaml.gz', 'openapi.yaml', 'discovery.json.gz', 'discovery.json'].map((f) => join(pkg, 'spec', f)).find((p) => existsSync(p));
409
431
  // a Protocol Buffers service publishes no examples in its .proto files: its examples are its documentation's alone
410
432
  if (!spec && existsSync(join(pkg, 'spec', 'proto')))
411
433
  return [];
@@ -420,22 +442,6 @@ function fromSpec(pkg) {
420
442
  if (!spec && !documents)
421
443
  throw new Error(`${pkg}: no published-example reader for this spec yet`);
422
444
  const text = spec ? (spec.endsWith('.gz') ? gunzipSync(readFileSync(spec)) : readFileSync(spec)).toString('utf8') : '';
423
- const out = [];
424
- if (spec && /smithy\.json(\.gz)?$/.test(spec)) {
425
- const model = parseExactJson(text);
426
- const restXml = Object.values(model.shapes).some((sh) => sh.type === 'service' && sh.traits?.['aws.protocols#restXml'] !== undefined);
427
- if (restXml)
428
- xmlModel = model;
429
- for (const [id, shape] of Object.entries(model.shapes)) {
430
- const examples = shape.traits?.['smithy.api#examples'];
431
- const operation = id.split('#')[1];
432
- // an awsJson operation is named by its x-amz-target header, `<service>.<operation>`; its body is the input. A
433
- // restXml operation (S3's) is named by its method and path, and its input is where each member's binding puts it
434
- for (const e of examples ?? [])
435
- out.push({ id: `${operation}#${e.title}`, operation, output: e.error ? e.error.content : e.output, input: e.input ?? {}, ...(restXml ? { restXml: id } : { target: operation }) });
436
- }
437
- return out;
438
- }
439
445
  // a Google API Discovery document publishes no examples; it is read as the OpenAPI document derive-pack reads it as,
440
446
  // its patches applied to the Discovery document first (spec-ir-discovery.ts)
441
447
  const discovery = spec !== undefined && /discovery\.json(\.gz)?$/.test(spec);
@@ -454,8 +460,9 @@ function fromSpec(pkg) {
454
460
  continue;
455
461
  // Swagger 2: the request is the `in: body` parameter's schema, an answer its response's `schema`
456
462
  const bodyParam = (op.parameters ?? []).find((x) => x.in === 'body');
457
- schemasByOperation[idOf(op, method, path)] = specSchemas;
458
- operationSchemas[idOf(op, method, path)] = {
463
+ const key = operationKey(idOf(op, method, path), lane);
464
+ schemasByOperation[key] = specSchemas;
465
+ operationSchemas[key] = {
459
466
  request: json(op.requestBody?.content) ?? bodyParam?.schema,
460
467
  responses: Object.fromEntries(Object.entries(op.responses ?? {}).flatMap(([code, r]) => { const sch = json(r.content) ?? r.schema; return sch ? [[code, sch]] : []; })),
461
468
  };
@@ -978,17 +985,17 @@ function outputSchemaMismatches(schema, value, prefix, depth = 0) {
978
985
  * recorded with the per-example classification. Request echoes and generated output are absent. */
979
986
  const storedCollectionFields = {
980
987
  'ai-gateway': { listModels: ['data'] },
981
- aws: { ListBuckets: ['Buckets'], ListMultipartUploads: ['Uploads'], ListObjects: ['Contents', 'CommonPrefixes'], ListObjectsV2: ['Contents', 'CommonPrefixes'] },
982
- cloudflare: { ListBuckets: ['Buckets'], ListMultipartUploads: ['Uploads'], ListObjects: ['Contents', 'CommonPrefixes'], ListObjectsV2: ['Contents', 'CommonPrefixes'] },
988
+ aws: { 's3:ListBuckets': ['Buckets'], 's3:ListMultipartUploads': ['Uploads'], 's3:ListObjects': ['Contents', 'CommonPrefixes'], 's3:ListObjectsV2': ['Contents', 'CommonPrefixes'] },
989
+ cloudflare: { 'r2:ListBuckets': ['Buckets'], 'r2:ListMultipartUploads': ['Uploads'], 'r2:ListObjects': ['Contents', 'CommonPrefixes'], 'r2:ListObjectsV2': ['Contents', 'CommonPrefixes'] },
983
990
  discord: { oauth2_token: ['guild.roles'], get_my_oauth2_authorization: ['scopes'] },
984
991
  github: { 'repos/get-branch': ['commit.parents'], 'repos/get-commit': ['parents', 'files'], 'repos/get-content': ['', 'entries'], 'repos/compare-commits': ['commits', 'files'], 'git/get-tree': ['tree'], 'git/list-matching-refs': [''], 'repos/get-repo-rulesets': [''] },
985
- hackernews: { getItem: ['kids'], getUser: ['submitted'], getUpdates: ['items', 'profiles'], getTopStories: [''], getNewStories: [''], getBestStories: [''], getAskStories: [''], getShowStories: [''], getJobStories: [''] },
992
+ hackernews: { 'api:getItem': ['kids'], 'api:getUser': ['submitted'], 'api:getUpdates': ['items', 'profiles'], 'api:getTopStories': [''], 'api:getNewStories': [''], 'api:getBestStories': [''], 'api:getAskStories': [''], 'api:getShowStories': [''], 'api:getJobStories': [''] },
986
993
  linkedin: { findOrganizationAclsByRoleAssignee: ['elements'] },
987
994
  'npm-registry': { 'libnpmsearch.searchStream': ['objects', 'objects.[].package.maintainers'] },
988
995
  openrouter: { getCurrentKey: ['data.allowed_data_regions'], getModels: ['data'] },
989
996
  redis: { SCAN: ['[].1'] },
990
997
  resend: { 'domains/get': ['records'], 'emails/list-receiving': ['data', 'data.[].attachments'] },
991
- sentry: { listOrganizations: ['', '[].features'] },
998
+ sentry: { 'api:listOrganizations': ['', '[].features'] },
992
999
  slack: { conversations_rename: ['channel.previous_names'], conversations_invite: ['channel.previous_names'], conversations_members: ['members'], conversations_list: ['channels', 'channels.[].previous_names'], users_conversations: ['channels', 'channels.[].previous_names'], conversations_info: ['channel.previous_names'], conversations_replies: ['messages'], conversations_history: ['messages'], usergroups_list: ['usergroups'] },
993
1000
  tinybird: { datasources: ['datasources'], jobs: ['jobs'], pipes: ['pipes'], tokens: ['tokens'], token_get: ['scopes'], token_refresh: ['scopes'], pipe_set_endpoint: ['nodes'] },
994
1001
  };
@@ -1104,7 +1111,7 @@ if (!vendor || vendor.startsWith('-')) {
1104
1111
  const PKG = join(PACKS, vendor);
1105
1112
  const file = join(PKG, 'journeys', 'vendor-examples.json');
1106
1113
  await loadPlaceholders(PKG);
1107
- const examples = published(PKG);
1114
+ const examples = published(PKG, undefined);
1108
1115
  const byId = new Map(examples.map((e) => [e.id, e]));
1109
1116
  if (byId.size !== examples.length)
1110
1117
  throw new Error(`${vendor}: ${examples.length - byId.size} published examples share an id`);
@@ -1121,11 +1128,13 @@ const shape = await shapeChecker(PKG);
1121
1128
  for (const e of examples) {
1122
1129
  if (e.patchRemoved || !e.request || !shape)
1123
1130
  continue;
1124
- const routed = shape.operationOf(e.request.method, e.request.url, {})?.op.id;
1131
+ const hit = shape.operationOf(e.request.method, new URL(e.request.url, `http://${journey.host ?? 'twin.local'}`).href, e.request.headers);
1132
+ const routed = hit ? operationKey(hit.op.id, hit.lane) : undefined;
1125
1133
  if (e.operation === 'gap' && routed === undefined)
1126
1134
  continue;
1127
- if (routed !== e.operation)
1128
- throw new Error(`spec/doc-examples.json: "${e.id}" sends ${e.request.method} ${e.request.url}, which the spec routes to ${routed ?? 'no operation'}, not ${e.operation}`);
1135
+ const expected = operationKey(e.operation, e.lane);
1136
+ if (routed !== expected)
1137
+ throw new Error(`spec/doc-examples.json: "${e.id}" sends ${e.request.method} ${e.request.url}, which the spec routes to ${routed ?? 'no operation'}, not ${expected}`);
1129
1138
  }
1130
1139
  const sweeping = process.argv.includes('--sweep');
1131
1140
  const sweepHits = [];
@@ -1258,18 +1267,20 @@ await walk(journey, world.root(), failures, {
1258
1267
  after: (step, _path, answer, stepFailures) => {
1259
1268
  if (isGit(step) || isWait(step) || isSocket(step))
1260
1269
  return;
1261
- if (!isResp(step) && !isLine(step)) {
1270
+ const request = answer.request;
1271
+ const hit = request ? shape?.operationOf(request.method, request.url, request.headers) : undefined;
1272
+ const operation = hit?.op;
1273
+ if (request) {
1262
1274
  const st = step;
1263
- const method = (st.method ?? 'GET').toUpperCase();
1275
+ const method = request.method;
1264
1276
  // a read by what the operation does (an RPC wire POSTs its reads), else by its verb
1265
- const op = shape?.operationOf(method, _path, { ...(journey.headers ?? {}), ...(st.headers ?? {}) })?.op;
1266
- const opClass = op?.class;
1277
+ const opClass = operation?.class;
1267
1278
  const isRead = opClass ? ['retrieve', 'list', 'computed'].includes(opClass) : method === 'GET';
1268
1279
  if (isRead)
1269
- reads.push({ path: bare(_path), body: JSON.stringify(answer.body ?? '') });
1280
+ reads.push({ path: bare(request.url), body: JSON.stringify(answer.body ?? '') });
1270
1281
  else if (st.example && answer.status < 300 && answer.body && typeof answer.body === 'object' && !Array.isArray(answer.body) && Object.keys(answer.body).length <= 3) {
1271
1282
  const body = answer.body;
1272
- const segs = bare(_path).split('/');
1283
+ const segs = bare(request.url).split('/');
1273
1284
  // an action on a subject (`/emails/{id}/cancel`) touches the subject; a create touches the new id under the collection;
1274
1285
  // a write whose subject is neither minted nor in its path (an RPC call naming it in its body) is left to the judge
1275
1286
  const minted = typeof body.id === 'string' ? body.id : '';
@@ -1277,8 +1288,7 @@ await walk(journey, world.root(), failures, {
1277
1288
  if (minted || idInPath) {
1278
1289
  const base = minted && !segs.includes(minted) ? `${segs.join('/')}/${minted}` : /^[a-z_-]+$/.test(segs.at(-1) ?? '') && segs.length > 2 ? segs.slice(0, -1).join('/') : segs.join('/');
1279
1290
  // only an effect the vendor gives a door to read: a GET of the subject or of its collection (a moderation has neither)
1280
- const hdrs = { ...(journey.headers ?? {}), ...(st.headers ?? {}) };
1281
- const gets = (at) => shape?.operationOf('GET', at, hdrs)?.op.method.toUpperCase() === 'GET';
1291
+ const gets = (at) => shape?.operationOf('GET', new URL(at, request.url).href, request.headers)?.op.method.toUpperCase() === 'GET';
1282
1292
  const door = !shape || gets(base) || gets(base.split('/').slice(0, -1).join('/'));
1283
1293
  if (door)
1284
1294
  thinWrites.push({ step: st.name, base, id: base.split('/').at(-1) ?? '', from: reads.length, deletes: method === 'DELETE' || opClass === 'delete' });
@@ -1308,20 +1318,20 @@ await walk(journey, world.root(), failures, {
1308
1318
  unknown.push(s.example);
1309
1319
  return;
1310
1320
  }
1311
- specSchemas = schemasByOperation[ex.operation] ?? {};
1321
+ const key = operationKey(ex.operation, ex.lane);
1322
+ specSchemas = schemasByOperation[key] ?? {};
1312
1323
  replayed.add(s.example);
1313
- if (ex.operation === 'gap' && ex.request) {
1314
- const sent = new URL(_path, `https://${journey.host}`);
1315
- if (s.method !== ex.request.method || sent.href !== ex.request.url)
1324
+ if (ex.operation === 'gap' && ex.request && request) {
1325
+ if (request.method !== ex.request.method || request.url !== ex.request.url)
1316
1326
  failures.push({ step: s.name, beat: 'EXAMPLE', detail: 'the unknown request differs from its recorded method or URL' });
1317
1327
  }
1318
- if (ex.authorizationAbsent && Object.keys({ ...(journey.headers ?? {}), ...(s.headers ?? {}) }).some((key) => key.toLowerCase() === 'authorization'))
1328
+ if (ex.authorizationAbsent && request && new Headers(request.headers).has('authorization'))
1319
1329
  failures.push({ step: s.name, beat: 'EXAMPLE', detail: 'the recording has no Authorization header, but the replay sends one' });
1320
1330
  // the step calls the example's operation (the spec's, as the walk's SHAPE check reads the request)
1321
- if (!isResp(step) && !isLine(step) && shape) {
1322
- const called = shape.operationOf(s.method, _path, { ...(journey.headers ?? {}), ...(s.headers ?? {}) })?.op.id;
1323
- if (called && called !== ex.operation)
1324
- failures.push({ step: s.name, beat: 'EXAMPLE', detail: `calls ${called}, not the example's operation ${ex.operation}` });
1331
+ if (request && shape) {
1332
+ const called = hit ? operationKey(hit.op.id, hit.lane) : undefined;
1333
+ if (called && called !== key)
1334
+ failures.push({ step: s.name, beat: 'EXAMPLE', detail: `calls ${called}, not the example's operation ${key}` });
1325
1335
  }
1326
1336
  // the example is sent as published: its input as the body, to its operation
1327
1337
  // a published field the step leaves out (`unsent`: a rendered value naming an object the World does not hold) must
@@ -1329,26 +1339,28 @@ await walk(journey, world.root(), failures, {
1329
1339
  for (const [path, why] of Object.entries(s.unsent ?? {})) {
1330
1340
  if (!why)
1331
1341
  failures.push({ step: s.name, beat: 'EXAMPLE', detail: `unsent ${path} gives no reason` });
1332
- if (requiredAt(operationSchemas[ex.operation]?.request, path))
1342
+ if (requiredAt(operationSchemas[key]?.request, path))
1333
1343
  failures.push({ step: s.name, beat: 'EXAMPLE', detail: `unsent ${path} is required by the operation's request schema` });
1334
1344
  }
1335
1345
  const publishedBody = exampleBody({ ...s, body: ex.input, form: undefined, bodyBase64: undefined }, ex.input);
1336
1346
  const inputType = Object.entries(ex.request?.headers ?? {}).find(([k]) => k.toLowerCase() === 'content-type')?.[1]
1337
- ?? Object.entries({ ...journey.headers, ...s.headers }).find(([k]) => k.toLowerCase() === 'content-type')?.[1];
1347
+ ?? (request ? new Headers(request.headers).get('content-type') ?? undefined : undefined);
1338
1348
  const publishedInput = s.unsent && publishedBody !== undefined ? withoutPaths(publishedBody, Object.keys(s.unsent)) : publishedBody;
1339
1349
  // the body the step sends, every check below reading it alike: no body (a GET) is the empty input a page publishes for it
1340
1350
  const body = isResp(s) ? s.resp : exampleBody(s, publishedInput) ?? {};
1341
1351
  // A placeholder used as a request-map key is bound by its scalar request position, just as an answer key is.
1342
1352
  const input = answerKeys(publishedInput, publishedInput, body, ex.placeholder, true);
1343
- if (ex.restXml)
1344
- for (const p of restXmlInputProblems(ex.restXml, (input ?? {}), shape?.pathOf?.(_path) ?? _path, { ...(journey.headers ?? {}), ...(s.headers ?? {}) }, placeholderIds, s.path))
1345
- failures.push({ step: s.name, beat: 'EXAMPLE', detail: `the request is not the example's published input: ${p}` });
1353
+ if (ex.restXml) {
1354
+ if (request)
1355
+ for (const p of restXmlInputProblems(ex.restXml, (input ?? {}), shape?.pathOf(request.method, request.url, request.headers) ?? request.url, request.headers, placeholderIds, s.path))
1356
+ failures.push({ step: s.name, beat: 'EXAMPLE', detail: `the request is not the example's published input: ${p}` });
1357
+ }
1346
1358
  else if (input !== undefined && (!requestMapKeysHold(publishedInput, input, body, ex.placeholder) || (ex.partial ? !holds(body, input, ex.placeholder, undefined, ex.names, false, inputType) : !same(body, input, ex.placeholder ?? /(?!)/, undefined, ex.names, false, inputType))))
1347
1359
  failures.push({ step: s.name, beat: 'EXAMPLE', detail: typeof input === 'string' ? comparedText(body, input, ex.placeholder ?? /(?!)/, undefined, ex.names, inputType)?.problem ?? "the body is not the example's published input" : "the body is not the example's published input" });
1348
1360
  // a scenario's turn is sent as the RFC writes it: its client lines (none for the greeting, a connect)
1349
1361
  if (ex.lines && JSON.stringify([s.send ?? []].flat()) !== JSON.stringify(ex.lines))
1350
1362
  failures.push({ step: s.name, beat: 'EXAMPLE', detail: `sends ${JSON.stringify(s.send ?? null)}, not the scenario's ${JSON.stringify(ex.lines)}` });
1351
- const target = Object.entries({ ...journey.headers, ...s.headers }).find(([k]) => k.toLowerCase() === 'x-amz-target')?.[1];
1363
+ const target = request ? new Headers(request.headers).get('x-amz-target') : undefined;
1352
1364
  if (ex.target && !target?.endsWith(`.${ex.target}`))
1353
1365
  failures.push({ step: s.name, beat: 'EXAMPLE', detail: `x-amz-target is ${target}, not the example's operation ${ex.target}` });
1354
1366
  if (ex.status && answer.status !== ex.status && !s.exampleDiffers?.status)
@@ -1391,9 +1403,8 @@ await walk(journey, world.root(), failures, {
1391
1403
  const published = isResp(s) && publishedValue !== undefined ? (Array.isArray(s.resp?.[0]) ? publishedValue : [publishedValue]) : publishedValue;
1392
1404
  const responseType = answer.headers?.['content-type'];
1393
1405
  const literal = isResp(s) || (published !== null && typeof published !== 'object');
1394
- const valueSchema = ex.schema ?? operationSchemas[ex.operation]?.responses[String(answer.status)];
1395
- comparisonCollections = [...(storedCollectionFields[vendor]?.[ex.operation] ?? [])];
1396
- const operation = shape?.operationOf((s.method ?? 'GET').toUpperCase(), _path, { ...(journey.headers ?? {}), ...(s.headers ?? {}) })?.op;
1406
+ const valueSchema = ex.schema ?? operationSchemas[key]?.responses[String(answer.status)];
1407
+ comparisonCollections = [...(storedCollectionFields[vendor]?.[key] ?? [])];
1397
1408
  // A derived resource list names its holder; never treat every array under a read as a list.
1398
1409
  if (operation?.class === 'list' && operation.answers?.list) {
1399
1410
  const holder = operation.answers.key ? published?.[operation.answers.key] : published;
@@ -1439,7 +1450,7 @@ await walk(journey, world.root(), failures, {
1439
1450
  },
1440
1451
  });
1441
1452
  const owners = (await vendorFetch(PACKS, vendor))?.(world.root())?.owners?.() ?? (await vendorStream(PACKS, vendor))?.(world.root())?.owners?.() ?? {};
1442
- const ownerOf = (e) => e.wire === 'client' ? 'gap' : owners[e.lane ? `${e.lane}:${e.operation}` : e.operation];
1453
+ const ownerOf = (e) => e.wire === 'client' ? 'gap' : owners[operationKey(e.operation, e.lane)];
1443
1454
  const patchRemoved = examples.filter((e) => e.patchRemoved);
1444
1455
  const unserved = examples.filter((e) => !e.patchRemoved && ownerOf(e) === 'gap');
1445
1456
  const recorded = Object.keys(journey.notReplayed ?? {});