@volter/twin-standard 1.0.158 → 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 (69) hide show
  1. package/README.md +3 -2
  2. package/client-sdks/package.json +1 -0
  3. package/client-sdks/pnpm-lock.yaml +210 -0
  4. package/dist/client-sdks/package.json +1 -0
  5. package/dist/client-sdks/pnpm-lock.yaml +210 -0
  6. package/dist/src/assessment.d.ts +13 -3
  7. package/dist/src/assessment.js +10 -6
  8. package/dist/src/browser-assessment.js +16 -13
  9. package/dist/src/clients/harness.d.ts +25 -4
  10. package/dist/src/clients/harness.js +184 -22
  11. package/dist/src/clients/jira.js +11 -2
  12. package/dist/src/clients/run.js +19 -14
  13. package/dist/src/conformance.d.ts +2 -0
  14. package/dist/src/conformance.js +392 -184
  15. package/dist/src/consumer.d.ts +22 -1
  16. package/dist/src/consumer.js +91 -26
  17. package/dist/src/credential-issuer-import.d.ts +3 -0
  18. package/dist/src/credential-issuer-import.js +42 -0
  19. package/dist/src/derive.js +3 -0
  20. package/dist/src/gate.js +4 -0
  21. package/dist/src/lanes.d.ts +2 -0
  22. package/dist/src/lanes.js +7 -0
  23. package/dist/src/protocol-3.js +8 -3
  24. package/dist/src/spec-documents.d.ts +4 -0
  25. package/dist/src/spec-documents.js +110 -3
  26. package/dist/src/tools/life-coverage-walk.js +38 -26
  27. package/dist/src/tools/score-pack.js +6 -5
  28. package/dist/src/tools/vendor-examples.js +91 -80
  29. package/dist/src/walk/behavior-journey.d.ts +22 -14
  30. package/dist/src/walk/behavior-journey.js +182 -133
  31. package/dist/src/walk/journey-kit.d.ts +3 -2
  32. package/dist/src/walk/journey-kit.js +6 -2
  33. package/dist/src/walk/replay.d.ts +23 -0
  34. package/dist/src/walk/replay.js +118 -0
  35. package/dist/src/walk/serve-pack.d.ts +2 -2
  36. package/dist/src/walk/serve-pack.js +38 -2
  37. package/dist/src/walk/shape.d.ts +25 -4
  38. package/dist/src/walk/shape.js +166 -56
  39. package/dist/src/walk/vendor-fetch.d.ts +7 -2
  40. package/dist/src/walk/vendor-fetch.js +13 -2
  41. package/dist/src/walk/walk-database.d.ts +9 -1
  42. package/dist/src/walk/walk-database.js +35 -13
  43. package/dist/src/walk/xml-view.d.ts +3 -2
  44. package/dist/src/walk/xml-view.js +5 -4
  45. package/package.json +3 -3
  46. package/src/assessment.ts +12 -8
  47. package/src/browser-assessment.ts +12 -5
  48. package/src/clients/harness.ts +142 -19
  49. package/src/clients/jira.ts +4 -2
  50. package/src/clients/run.ts +17 -15
  51. package/src/conformance.ts +313 -128
  52. package/src/consumer.ts +83 -21
  53. package/src/credential-issuer-import.ts +38 -0
  54. package/src/derive.ts +3 -1
  55. package/src/gate.ts +3 -0
  56. package/src/lanes.ts +7 -0
  57. package/src/protocol-3.ts +7 -3
  58. package/src/spec-documents.ts +92 -3
  59. package/src/tools/life-coverage-walk.ts +32 -22
  60. package/src/tools/score-pack.ts +7 -6
  61. package/src/tools/vendor-examples.ts +88 -75
  62. package/src/walk/behavior-journey.ts +145 -109
  63. package/src/walk/journey-kit.ts +7 -5
  64. package/src/walk/replay.ts +103 -0
  65. package/src/walk/serve-pack.ts +25 -5
  66. package/src/walk/shape.ts +156 -54
  67. package/src/walk/vendor-fetch.ts +16 -4
  68. package/src/walk/walk-database.ts +41 -15
  69. package/src/walk/xml-view.ts +5 -4
@@ -48,7 +48,7 @@ import { basename, join, resolve } from 'node:path';
48
48
  import { gunzipSync } from 'node:zlib';
49
49
  import { inMemoryWorld, walk, type Failure, usePacks } from '../walk/behavior-journey.ts';
50
50
  import { vendorFetch, vendorStream } from '../walk/vendor-fetch.ts';
51
- import { at as selectorAt, captureAt, expandFills, isResp, isGit, isLine, isSocket, isWait, type Journey, type JourneyStep, type LineStep } from '../walk/journey-kit.ts';
51
+ import { at as selectorAt, captureAt, expandFills, isResp, isGit, isSocket, isWait, type Journey, type JourneyStep, type LineStep } from '../walk/journey-kit.ts';
52
52
  import { lanesOf, shapeChecker } from '../walk/shape.ts';
53
53
  import { applySpecPatch, discoveryToOpenAPI, mergeDocuments, readSpecDocuments, slug, type SpecPatch } from '../spec.ts';
54
54
  import { packsRoot } from './packs-root.ts';
@@ -63,7 +63,7 @@ usePacks(PACKS);
63
63
  * still be the sample's type. `schema` is the served spec's schema of the answer, which says where null is allowed. */
64
64
  /** `placeholder`: strings of the published input matching it stand for ids the World mints (openai's `vs_abc123`), so the
65
65
  * step sends a string there, not that one. */
66
- type Example = { names?: string[]; wire?: 'client'; patchRemoved?: boolean; id: string; operation: string; lane?: string; status?: number; output: unknown; input?: unknown; partial?: boolean; placeholder?: RegExp; target?: string; restXml?: string; lines?: string[]; sample?: boolean; schema?: SpecSchema; request?: { method: string; url: string; headers?: Record<string, string> }; authorizationAbsent?: boolean };
66
+ type Example = { names?: string[]; wire?: 'client'; patchRemoved?: boolean; id: string; operation: string; lane?: string; status?: number; output: unknown; input?: unknown; partial?: boolean; placeholder?: RegExp; target?: string; restXml?: RestXml; lines?: string[]; sample?: boolean; schema?: SpecSchema; request?: { method: string; url: string; headers?: Record<string, string> }; authorizationAbsent?: boolean };
67
67
  /** A served spec's schema, its references resolved lazily (`resolveSchema`). */
68
68
  type SpecSchema = { $ref?: string; nullable?: boolean; properties?: Record<string, SpecSchema>; items?: SpecSchema; allOf?: SpecSchema[]; anyOf?: SpecSchema[]; oneOf?: SpecSchema[]; [k: string]: unknown };
69
69
  let specSchemas: Record<string, SpecSchema> = {};
@@ -71,6 +71,7 @@ let specSchemas: Record<string, SpecSchema> = {};
71
71
  const schemasByOperation: Record<string, Record<string, SpecSchema>> = {};
72
72
  /** Each operation's request-body schema and its JSON answers' schemas by status, as the served spec gives them. */
73
73
  const operationSchemas: Record<string, { request?: SpecSchema; responses: Record<string, SpecSchema> }> = {};
74
+ const operationKey = (operation: string, lane: string | undefined): string => lane ? `${lane}:${operation}` : operation;
74
75
 
75
76
  /** A page's placeholder id in an answer-map key is the id sent at the corresponding published request position.
76
77
  * Only declared placeholders bind, and an ambiguous repeated placeholder cannot stand for a guessed map key. */
@@ -107,7 +108,9 @@ function served<T extends object>(pkg: string, doc: T): T {
107
108
 
108
109
  // ── a restXml service's examples (S3's): the input where each member's binding puts it, the answer read by its shape ──
109
110
  type Json = any;
110
- let xmlModel: { shapes: Record<string, Json> } | undefined;
111
+ type SmithyModel = { shapes: Record<string, Json> };
112
+ type RestXml = { id: string; model: SmithyModel };
113
+ const restXmlService = (model: SmithyModel): boolean => Object.values(model.shapes).some((shape) => shape.type === 'service' && shape.traits?.['aws.protocols#restXml'] !== undefined);
111
114
  /** A translated placeholder-key map is a complete map even when its surrounding documented request is partial. */
112
115
  function requestMapKeysHold(published: unknown, translated: unknown, sent: unknown, placeholder?: RegExp): boolean {
113
116
  if (!placeholder || !published || typeof published !== 'object') return true;
@@ -122,10 +125,10 @@ function requestMapKeysHold(published: unknown, translated: unknown, sent: unkno
122
125
  }
123
126
 
124
127
  const PRELUDE: Record<string, string> = { Boolean: 'boolean', PrimitiveBoolean: 'boolean', Integer: 'integer', PrimitiveInteger: 'integer', Long: 'long', PrimitiveLong: 'long', Float: 'float', Double: 'double' };
125
- const scalarType = (target: string): string => xmlModel?.shapes[target]?.type ?? PRELUDE[target.split('#')[1] ?? ''] ?? 'string';
126
- const scalarOf = (target: string, v: unknown): unknown => {
128
+ const scalarType = (model: SmithyModel, target: string): string => model.shapes[target]?.type ?? PRELUDE[target.split('#')[1] ?? ''] ?? 'string';
129
+ const scalarOf = (model: SmithyModel, target: string, v: unknown): unknown => {
127
130
  if (typeof v !== 'string') return v;
128
- const t = scalarType(target);
131
+ const t = scalarType(model, target);
129
132
  if (t === 'boolean') return v === 'true';
130
133
  if (['integer', 'long', 'short', 'byte'].includes(t)) return /^[+-]?\d+$/.test(v) ? exactInteger(v) : v;
131
134
  return ['float', 'double'].includes(t) && v !== '' && !Number.isNaN(Number(v)) ? Number(v) : v;
@@ -134,39 +137,39 @@ const arrayOf = (v: unknown): unknown[] => (v === undefined || v === '' ? [] : A
134
137
 
135
138
  /** An answer's XML members (xml-view) read by the model's shape: each member under its xmlName, a flattened list the
136
139
  * repeated element, a wrapped list its container's member elements, scalars as their shape's type. */
137
- function typedXml(target: string, v: unknown): unknown {
138
- const shape = xmlModel?.shapes[target];
139
- if (!shape || shape.type !== 'structure') return scalarOf(target, v);
140
+ function typedXml(model: SmithyModel, target: string, v: unknown): unknown {
141
+ const shape = model.shapes[target];
142
+ if (!shape || shape.type !== 'structure') return scalarOf(model, target, v);
140
143
  if (!v || typeof v !== 'object') return v === '' ? {} : v;
141
144
  const obj = v as Record<string, unknown>;
142
145
  const out: Record<string, unknown> = {};
143
146
  for (const [name, m] of Object.entries((shape.members ?? {}) as Record<string, Json>)) {
144
147
  if (Object.keys(m.traits ?? {}).some((t) => /^smithy\.api#http(Header|PrefixHeaders|ResponseCode)$/.test(t))) continue;
145
148
  const el = String(m.traits?.['smithy.api#xmlName'] ?? name);
146
- const ts = xmlModel!.shapes[m.target];
149
+ const ts = model.shapes[m.target];
147
150
  const raw = obj[el];
148
151
  if (ts?.type === 'list') {
149
- if (m.traits?.['smithy.api#xmlFlattened'] !== undefined) { if (raw !== undefined) out[name] = arrayOf(raw).map((i) => typedXml(ts.member.target, i)); continue; }
152
+ if (m.traits?.['smithy.api#xmlFlattened'] !== undefined) { if (raw !== undefined) out[name] = arrayOf(raw).map((i) => typedXml(model, ts.member.target, i)); continue; }
150
153
  if (raw === undefined) continue;
151
154
  const memberEl = String(ts.member.traits?.['smithy.api#xmlName'] ?? 'member');
152
- out[name] = arrayOf(raw === '' ? undefined : (raw as Record<string, unknown>)[memberEl]).map((i) => typedXml(ts.member.target, i));
153
- } else if (raw !== undefined) out[name] = typedXml(m.target, raw);
155
+ out[name] = arrayOf(raw === '' ? undefined : (raw as Record<string, unknown>)[memberEl]).map((i) => typedXml(model, ts.member.target, i));
156
+ } else if (raw !== undefined) out[name] = typedXml(model, m.target, raw);
154
157
  }
155
158
  return out;
156
159
  }
157
160
 
158
161
  /** A restXml operation's success answer as its output structure: the body's members by shape (or its payload
159
162
  * member's), and the members its headers carry. */
160
- function restXmlAnswer(opId: string, body: unknown, headers: Record<string, string> = {}): unknown {
161
- const out = xmlModel!.shapes[xmlModel!.shapes[opId]?.output?.target ?? ''];
163
+ function restXmlAnswer({ id: opId, model }: RestXml, body: unknown, headers: Record<string, string> = {}): unknown {
164
+ const out = model.shapes[model.shapes[opId]?.output?.target ?? ''];
162
165
  if (!out) return body;
163
166
  const members = Object.entries((out.members ?? {}) as Record<string, Json>);
164
167
  const payload = members.find(([, m]) => m.traits?.['smithy.api#httpPayload'] !== undefined);
165
- const answer = (payload ? { [payload[0]]: typedXml(payload[1].target, body) } : typedXml(xmlModel!.shapes[opId].output.target, body)) as Record<string, unknown>;
168
+ const answer = (payload ? { [payload[0]]: typedXml(model, payload[1].target, body) } : typedXml(model, model.shapes[opId].output.target, body)) as Record<string, unknown>;
166
169
  const lower = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]));
167
170
  for (const [name, m] of members) {
168
171
  const h = m.traits?.['smithy.api#httpHeader'];
169
- if (h !== undefined && lower[String(h).toLowerCase()] !== undefined) answer[name] = scalarOf(m.target, lower[String(h).toLowerCase()]);
172
+ if (h !== undefined && lower[String(h).toLowerCase()] !== undefined) answer[name] = scalarOf(model, m.target, lower[String(h).toLowerCase()]);
170
173
  // a map bound to a header prefix (S3's Metadata, `x-amz-meta-*`): every header under it, by the rest of its name
171
174
  const prefix = m.traits?.['smithy.api#httpPrefixHeaders'];
172
175
  if (prefix !== undefined) answer[name] = Object.fromEntries(Object.entries(lower).filter(([k]) => k.startsWith(String(prefix).toLowerCase())).map(([k, v]) => [k.slice(String(prefix).length), v]));
@@ -176,8 +179,8 @@ function restXmlAnswer(opId: string, body: unknown, headers: Record<string, stri
176
179
 
177
180
  /** Where a restXml example's input is not what the step sends: each member the example gives, at its binding (a path
178
181
  * label, a query key, a header; a payload or body member is the step's body and is not compared). */
179
- function restXmlInputProblems(opId: string, input: Record<string, unknown>, path: string, headers: Record<string, string>, placeholder: RegExp, rawPath?: string): string[] {
180
- const shape = xmlModel!.shapes[xmlModel!.shapes[opId]?.input?.target ?? ''];
182
+ function restXmlInputProblems({ id: opId, model }: RestXml, input: Record<string, unknown>, path: string, headers: Record<string, string>, placeholder: RegExp, rawPath?: string): string[] {
183
+ const shape = model.shapes[model.shapes[opId]?.input?.target ?? ''];
181
184
  const url = new URL(path, 'http://twin.local');
182
185
  // the step as written, its captures unfilled: a value it takes from a capture is the World's own id (an S3 upload's
183
186
  // UploadId), which stands for the example's literal as a spec's ids stand for the World's (architecture, "Published examples")
@@ -215,11 +218,13 @@ function lenientJson(text: string): string {
215
218
  * response's `example`/`examples` (a `$ref` to `components.examples` followed), a Swagger 2 response's `examples`, Stripe's
216
219
  * 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
217
220
  * kept as text and compared with the published framing and payload). */
218
- function published(pkg: string): Example[] {
221
+ function published(pkg: string, lane: string | undefined): Example[] {
219
222
  // Both the vendor API and its lanes publish examples; a root spec must not hide a lane. A lane's example names its
220
223
  // lane, the operation being the lane's own (its owner is `<lane>:<id>` in the vendor's owners)
221
- const own = [...(hasOwnSurface(pkg) ? fromSpec(pkg) : []), ...fromDocs(pkg)];
222
- return [...own, ...lanesOf(pkg).flatMap((lane) => published(lane).map((e) => ({ ...e, lane: e.lane ? `${basename(lane)}:${e.lane}` : basename(lane) })))];
224
+ const smithy = ['smithy.json.gz', 'smithy.json'].map((name) => join(pkg, 'spec', name)).find(existsSync);
225
+ const model = smithy ? parseExactJson((smithy.endsWith('.gz') ? gunzipSync(readFileSync(smithy)) : readFileSync(smithy)).toString('utf8')) as SmithyModel : undefined;
226
+ const own = [...(hasOwnSurface(pkg) ? fromSpec(pkg, lane, model) : []), ...fromDocs(pkg, lane, model)].map((e) => ({ ...e, lane }));
227
+ return [...own, ...lanesOf(pkg).flatMap((dir) => published(dir, lane ? `${lane}:${basename(dir)}` : basename(dir)))];
223
228
  }
224
229
 
225
230
  /** The examples a vendor's documentation pages publish beside its spec (a schedule page's expressions, each with what
@@ -231,7 +236,7 @@ function published(pkg: string): Example[] {
231
236
  * reference writes every property, and every scalar as a string, `"verified": "true"` beside a schema saying boolean)
232
237
  * shows what the answer can hold, not one request's answer: its example is a sample against the served schema, and a
233
238
  * string where the schema says number, integer or boolean is read as that type, in the answer and the request body. */
234
- function fromDocs(pkg: string): Example[] {
239
+ function fromDocs(pkg: string, lane: string | undefined, model: SmithyModel | undefined): Example[] {
235
240
  const file = join(pkg, 'spec', 'doc-examples.json');
236
241
  if (!existsSync(file)) return [];
237
242
  const docs = parseExactJson(readFileSync(file, 'utf8')) as Array<{ wire?: 'client'; title: string; source: string; quote?: string; operation: string; input?: unknown; output?: unknown; means: string; status?: number; rendered?: boolean; placeholders?: { names: string[]; quote: string }; request?: { method: string; url: string; headers?: Record<string, string> } }>;
@@ -261,11 +266,10 @@ function fromDocs(pkg: string): Example[] {
261
266
  // a page that names its own variables for the reader to fill ("Make sure to fill in the … BRANCH_NAME … variables")
262
267
  // lists them with that sentence quoted: each stands for any value, and the published text stays as written
263
268
  for (const d of docs) if (d.placeholders && (!d.placeholders.names?.length || !d.placeholders.quote?.trim())) throw new Error(`${file}: "${d.title}" names placeholders without the page's words for them (placeholders.quote)`);
264
- const smithy = ['smithy.json.gz', 'smithy.json'].some((f) => existsSync(join(pkg, 'spec', f)));
265
269
  // a page's example is a fragment (an expression, not the request that carries it): the body holds it
266
270
  return docs.map((d) => {
267
271
  if (!d.rendered) return fromPage(d, d.output, d.input);
268
- const schemas = operationSchemas[d.operation];
272
+ const schemas = operationSchemas[operationKey(d.operation, lane)];
269
273
  const schema = schemas?.responses[String(d.status ?? 200)];
270
274
  if (!schema) throw new Error(`${file}: "${d.title}" is rendered from a schema the served spec does not give (${d.operation}, ${d.status ?? 200})`);
271
275
  // the renderer writes any string it cannot fill as the word "string": in a rendered input it stands for any string
@@ -276,7 +280,13 @@ function fromDocs(pkg: string): Example[] {
276
280
  function fromPage(d: (typeof docs)[number], output: unknown, input: unknown): Example {
277
281
  const named = d.placeholders?.names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
278
282
  const placeholder = named ? new RegExp(`${placeholderIds.source}|^(?:${named})$`, placeholderIds.flags) : placeholderIds;
279
- 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 } : {}) };
283
+ let restXml: RestXml | undefined;
284
+ if (model && restXmlService(model)) {
285
+ const id = Object.keys(model.shapes).find((key) => key.endsWith(`#${d.operation}`));
286
+ if (!id) throw new Error(`${file}: "${d.title}" names no operation in its XML service (${d.operation})`);
287
+ restXml = { id, model };
288
+ }
289
+ 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 } : {}) };
280
290
  }
281
291
  }
282
292
 
@@ -351,11 +361,23 @@ function decodedExample(media: string, value: unknown): unknown {
351
361
  try { return parseExactJson(value); } catch { return value; }
352
362
  }
353
363
 
354
- function fromSpec(pkg: string): Example[] {
364
+ function fromSpec(pkg: string, lane: string | undefined, model: SmithyModel | undefined): Example[] {
355
365
  const rfc = existsSync(join(pkg, 'spec')) ? readdirSync(join(pkg, 'spec')).find((f) => /^rfc\d+\.txt$/.test(f)) : undefined;
356
366
  if (rfc) return scenarios(readFileSync(join(pkg, 'spec', rfc), 'utf8'));
367
+ const out: Example[] = [];
368
+ if (model) {
369
+ const restXml = restXmlService(model);
370
+ for (const [id, shape] of Object.entries(model.shapes)) {
371
+ const examples = shape.traits?.['smithy.api#examples'] as Array<{ title: string; input?: unknown; output?: unknown; error?: { content?: unknown } }> | undefined;
372
+ const operation = id.split('#')[1];
373
+ // an awsJson operation is named by its x-amz-target header, `<service>.<operation>`; its body is the input. A
374
+ // restXml operation (S3's) is named by its method and path, and its input is where each member's binding puts it
375
+ for (const e of examples ?? []) out.push({ id: `${operation}#${e.title}`, operation, output: e.error ? e.error.content : e.output, input: e.input ?? {}, ...(restXml ? { restXml: { id, model } } : { target: operation }) });
376
+ }
377
+ return out;
378
+ }
357
379
  // a spec vendored as published or gzipped, as derive-pack reads it
358
- 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));
380
+ 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));
359
381
  // a Protocol Buffers service publishes no examples in its .proto files: its examples are its documentation's alone
360
382
  if (!spec && existsSync(join(pkg, 'spec', 'proto'))) return [];
361
383
  // a command table (Redis's src/commands/*.json) publishes no examples either: its examples are its documentation's
@@ -366,20 +388,6 @@ function fromSpec(pkg: string): Example[] {
366
388
  const documents = spec ? undefined : readSpecDocuments(join(pkg, 'spec'));
367
389
  if (!spec && !documents) throw new Error(`${pkg}: no published-example reader for this spec yet`);
368
390
  const text = spec ? (spec.endsWith('.gz') ? gunzipSync(readFileSync(spec)) : readFileSync(spec)).toString('utf8') : '';
369
- const out: Example[] = [];
370
- if (spec && /smithy\.json(\.gz)?$/.test(spec)) {
371
- const model = parseExactJson(text) as { shapes: Record<string, { type?: string; traits?: Record<string, unknown> }> };
372
- const restXml = Object.values(model.shapes).some((sh) => sh.type === 'service' && sh.traits?.['aws.protocols#restXml'] !== undefined);
373
- if (restXml) xmlModel = model as { shapes: Record<string, Json> };
374
- for (const [id, shape] of Object.entries(model.shapes)) {
375
- const examples = shape.traits?.['smithy.api#examples'] as Array<{ title: string; input?: unknown; output?: unknown; error?: { content?: unknown } }> | undefined;
376
- const operation = id.split('#')[1];
377
- // an awsJson operation is named by its x-amz-target header, `<service>.<operation>`; its body is the input. A
378
- // restXml operation (S3's) is named by its method and path, and its input is where each member's binding puts it
379
- for (const e of examples ?? []) out.push({ id: `${operation}#${e.title}`, operation, output: e.error ? e.error.content : e.output, input: e.input ?? {}, ...(restXml ? { restXml: id } : { target: operation }) });
380
- }
381
- return out;
382
- }
383
391
  type Op = { operationId?: string; responses?: Record<string, Record<string, unknown>>; 'x-oaiMeta'?: { examples?: unknown } };
384
392
  type Doc = { paths: Record<string, Record<string, Op>>; components?: { examples?: Record<string, { value?: unknown }>; schemas?: Record<string, SpecSchema> }; definitions?: Record<string, SpecSchema> };
385
393
  // a Google API Discovery document publishes no examples; it is read as the OpenAPI document derive-pack reads it as,
@@ -399,8 +407,9 @@ function fromSpec(pkg: string): Example[] {
399
407
  if (!op || typeof op !== 'object' || !('responses' in op)) continue;
400
408
  // Swagger 2: the request is the `in: body` parameter's schema, an answer its response's `schema`
401
409
  const bodyParam = ((op as { parameters?: Array<{ in?: string; schema?: SpecSchema }> }).parameters ?? []).find((x) => x.in === 'body');
402
- schemasByOperation[idOf(op, method, path)] = specSchemas;
403
- operationSchemas[idOf(op, method, path)] = {
410
+ const key = operationKey(idOf(op, method, path), lane);
411
+ schemasByOperation[key] = specSchemas;
412
+ operationSchemas[key] = {
404
413
  request: json((op as { requestBody?: { content?: unknown } }).requestBody?.content) ?? bodyParam?.schema,
405
414
  responses: Object.fromEntries(Object.entries(op.responses ?? {}).flatMap(([code, r]) => { const sch = json(r.content) ?? (r as { schema?: SpecSchema }).schema; return sch ? [[code, sch]] : []; })),
406
415
  };
@@ -829,17 +838,17 @@ function outputSchemaMismatches(schema: SpecSchema | undefined, value: unknown,
829
838
  * recorded with the per-example classification. Request echoes and generated output are absent. */
830
839
  const storedCollectionFields: Record<string, Record<string, string[]>> = {
831
840
  'ai-gateway': { listModels: ['data'] },
832
- aws: { ListBuckets: ['Buckets'], ListMultipartUploads: ['Uploads'], ListObjects: ['Contents', 'CommonPrefixes'], ListObjectsV2: ['Contents', 'CommonPrefixes'] },
833
- cloudflare: { ListBuckets: ['Buckets'], ListMultipartUploads: ['Uploads'], ListObjects: ['Contents', 'CommonPrefixes'], ListObjectsV2: ['Contents', 'CommonPrefixes'] },
841
+ aws: { 's3:ListBuckets': ['Buckets'], 's3:ListMultipartUploads': ['Uploads'], 's3:ListObjects': ['Contents', 'CommonPrefixes'], 's3:ListObjectsV2': ['Contents', 'CommonPrefixes'] },
842
+ cloudflare: { 'r2:ListBuckets': ['Buckets'], 'r2:ListMultipartUploads': ['Uploads'], 'r2:ListObjects': ['Contents', 'CommonPrefixes'], 'r2:ListObjectsV2': ['Contents', 'CommonPrefixes'] },
834
843
  discord: { oauth2_token: ['guild.roles'], get_my_oauth2_authorization: ['scopes'] },
835
844
  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': [''] },
836
- hackernews: { getItem: ['kids'], getUser: ['submitted'], getUpdates: ['items', 'profiles'], getTopStories: [''], getNewStories: [''], getBestStories: [''], getAskStories: [''], getShowStories: [''], getJobStories: [''] },
845
+ hackernews: { 'api:getItem': ['kids'], 'api:getUser': ['submitted'], 'api:getUpdates': ['items', 'profiles'], 'api:getTopStories': [''], 'api:getNewStories': [''], 'api:getBestStories': [''], 'api:getAskStories': [''], 'api:getShowStories': [''], 'api:getJobStories': [''] },
837
846
  linkedin: { findOrganizationAclsByRoleAssignee: ['elements'] },
838
847
  'npm-registry': { 'libnpmsearch.searchStream': ['objects', 'objects.[].package.maintainers'] },
839
848
  openrouter: { getCurrentKey: ['data.allowed_data_regions'], getModels: ['data'] },
840
849
  redis: { SCAN: ['[].1'] },
841
850
  resend: { 'domains/get': ['records'], 'emails/list-receiving': ['data', 'data.[].attachments'] },
842
- sentry: { listOrganizations: ['', '[].features'] },
851
+ sentry: { 'api:listOrganizations': ['', '[].features'] },
843
852
  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'] },
844
853
  tinybird: { datasources: ['datasources'], jobs: ['jobs'], pipes: ['pipes'], tokens: ['tokens'], token_get: ['scopes'], token_refresh: ['scopes'], pipe_set_endpoint: ['nodes'] },
845
854
  };
@@ -931,7 +940,7 @@ if (!vendor || vendor.startsWith('-')) { console.error('usage: twin-standard exa
931
940
  const PKG = join(PACKS, vendor);
932
941
  const file = join(PKG, 'journeys', 'vendor-examples.json');
933
942
  await loadPlaceholders(PKG);
934
- const examples = published(PKG);
943
+ const examples = published(PKG, undefined);
935
944
  const byId = new Map(examples.map((e) => [e.id, e]));
936
945
 
937
946
  if (byId.size !== examples.length) throw new Error(`${vendor}: ${examples.length - byId.size} published examples share an id`);
@@ -948,9 +957,11 @@ const shape = await shapeChecker(PKG);
948
957
  // (a page of an operation the spec lacks can match another's route)
949
958
  for (const e of examples) {
950
959
  if (e.patchRemoved || !e.request || !shape) continue;
951
- const routed = shape.operationOf(e.request.method, e.request.url, {})?.op.id;
960
+ const hit = shape.operationOf(e.request.method, new URL(e.request.url, `http://${journey.host ?? 'twin.local'}`).href, e.request.headers);
961
+ const routed = hit ? operationKey(hit.op.id, hit.lane) : undefined;
952
962
  if (e.operation === 'gap' && routed === undefined) continue;
953
- if (routed !== e.operation) 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}`);
963
+ const expected = operationKey(e.operation, e.lane);
964
+ if (routed !== expected) 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}`);
954
965
  }
955
966
  const sweeping = process.argv.includes('--sweep');
956
967
  const sweepHits: string[] = [];
@@ -1065,17 +1076,19 @@ await walk(journey, world.root(), failures, {
1065
1076
  },
1066
1077
  after: (step, _path, answer, stepFailures) => {
1067
1078
  if (isGit(step) || isWait(step) || isSocket(step)) return;
1068
- if (!isResp(step) && !isLine(step as JourneyStep & LineStep)) {
1079
+ const request = answer.request;
1080
+ const hit = request ? shape?.operationOf(request.method, request.url, request.headers) : undefined;
1081
+ const operation = hit?.op;
1082
+ if (request) {
1069
1083
  const st = step as JourneyStep;
1070
- const method = (st.method ?? 'GET').toUpperCase();
1084
+ const method = request.method;
1071
1085
  // a read by what the operation does (an RPC wire POSTs its reads), else by its verb
1072
- const op = shape?.operationOf(method, _path, { ...(journey.headers ?? {}), ...(st.headers ?? {}) })?.op;
1073
- const opClass = op?.class;
1086
+ const opClass = operation?.class;
1074
1087
  const isRead = opClass ? ['retrieve', 'list', 'computed'].includes(opClass) : method === 'GET';
1075
- if (isRead) reads.push({ path: bare(_path), body: JSON.stringify(answer.body ?? '') });
1088
+ if (isRead) reads.push({ path: bare(request.url), body: JSON.stringify(answer.body ?? '') });
1076
1089
  else if (st.example && answer.status < 300 && answer.body && typeof answer.body === 'object' && !Array.isArray(answer.body) && Object.keys(answer.body).length <= 3) {
1077
1090
  const body = answer.body as Record<string, unknown>;
1078
- const segs = bare(_path).split('/');
1091
+ const segs = bare(request.url).split('/');
1079
1092
  // an action on a subject (`/emails/{id}/cancel`) touches the subject; a create touches the new id under the collection;
1080
1093
  // a write whose subject is neither minted nor in its path (an RPC call naming it in its body) is left to the judge
1081
1094
  const minted = typeof body.id === 'string' ? body.id : '';
@@ -1083,8 +1096,7 @@ await walk(journey, world.root(), failures, {
1083
1096
  if (minted || idInPath) {
1084
1097
  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('/');
1085
1098
  // only an effect the vendor gives a door to read: a GET of the subject or of its collection (a moderation has neither)
1086
- const hdrs = { ...(journey.headers ?? {}), ...(st.headers ?? {}) };
1087
- const gets = (at: string): boolean => shape?.operationOf('GET', at, hdrs)?.op.method.toUpperCase() === 'GET';
1099
+ const gets = (at: string): boolean => shape?.operationOf('GET', new URL(at, request.url).href, request.headers)?.op.method.toUpperCase() === 'GET';
1088
1100
  const door = !shape || gets(base) || gets(base.split('/').slice(0, -1).join('/'));
1089
1101
  if (door) thinWrites.push({ step: st.name, base, id: base.split('/').at(-1) ?? '', from: reads.length, deletes: method === 'DELETE' || opClass === 'delete' });
1090
1102
  }
@@ -1104,38 +1116,40 @@ await walk(journey, world.root(), failures, {
1104
1116
  if (!s.example) return;
1105
1117
  const ex = byId.get(s.example);
1106
1118
  if (!ex) { unknown.push(s.example); return; }
1107
- specSchemas = schemasByOperation[ex.operation] ?? {};
1119
+ const key = operationKey(ex.operation, ex.lane);
1120
+ specSchemas = schemasByOperation[key] ?? {};
1108
1121
  replayed.add(s.example);
1109
- if (ex.operation === 'gap' && ex.request) {
1110
- const sent = new URL(_path, `https://${journey.host}`);
1111
- if (s.method !== ex.request.method || sent.href !== ex.request.url) failures.push({ step: s.name, beat: 'EXAMPLE', detail: 'the unknown request differs from its recorded method or URL' });
1122
+ if (ex.operation === 'gap' && ex.request && request) {
1123
+ if (request.method !== ex.request.method || request.url !== ex.request.url) failures.push({ step: s.name, beat: 'EXAMPLE', detail: 'the unknown request differs from its recorded method or URL' });
1112
1124
  }
1113
- if (ex.authorizationAbsent && Object.keys({ ...(journey.headers ?? {}), ...(s.headers ?? {}) }).some((key) => key.toLowerCase() === 'authorization')) failures.push({ step: s.name, beat: 'EXAMPLE', detail: 'the recording has no Authorization header, but the replay sends one' });
1125
+ if (ex.authorizationAbsent && request && new Headers(request.headers).has('authorization')) failures.push({ step: s.name, beat: 'EXAMPLE', detail: 'the recording has no Authorization header, but the replay sends one' });
1114
1126
  // the step calls the example's operation (the spec's, as the walk's SHAPE check reads the request)
1115
- if (!isResp(step) && !isLine(step) && shape) {
1116
- const called = shape.operationOf(s.method, _path, { ...(journey.headers ?? {}), ...(s.headers ?? {}) })?.op.id;
1117
- if (called && called !== ex.operation) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `calls ${called}, not the example's operation ${ex.operation}` });
1127
+ if (request && shape) {
1128
+ const called = hit ? operationKey(hit.op.id, hit.lane) : undefined;
1129
+ if (called && called !== key) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `calls ${called}, not the example's operation ${key}` });
1118
1130
  }
1119
1131
  // the example is sent as published: its input as the body, to its operation
1120
1132
  // a published field the step leaves out (`unsent`: a rendered value naming an object the World does not hold) must
1121
1133
  // be one the operation's request schema does not require
1122
1134
  for (const [path, why] of Object.entries(s.unsent ?? {})) {
1123
1135
  if (!why) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `unsent ${path} gives no reason` });
1124
- if (requiredAt(operationSchemas[ex.operation]?.request, path)) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `unsent ${path} is required by the operation's request schema` });
1136
+ if (requiredAt(operationSchemas[key]?.request, path)) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `unsent ${path} is required by the operation's request schema` });
1125
1137
  }
1126
1138
  const publishedBody = exampleBody({ ...s, body: ex.input, form: undefined, bodyBase64: undefined }, ex.input);
1127
1139
  const inputType = Object.entries(ex.request?.headers ?? {}).find(([k]) => k.toLowerCase() === 'content-type')?.[1]
1128
- ?? Object.entries({ ...journey.headers, ...s.headers }).find(([k]) => k.toLowerCase() === 'content-type')?.[1];
1140
+ ?? (request ? new Headers(request.headers).get('content-type') ?? undefined : undefined);
1129
1141
  const publishedInput = s.unsent && publishedBody !== undefined ? withoutPaths(publishedBody, Object.keys(s.unsent)) : publishedBody;
1130
1142
  // the body the step sends, every check below reading it alike: no body (a GET) is the empty input a page publishes for it
1131
1143
  const body = isResp(s) ? s.resp : exampleBody(s, publishedInput) ?? {};
1132
1144
  // A placeholder used as a request-map key is bound by its scalar request position, just as an answer key is.
1133
1145
  const input = answerKeys(publishedInput, publishedInput, body, ex.placeholder, true);
1134
- if (ex.restXml) for (const p of restXmlInputProblems(ex.restXml, (input ?? {}) as Record<string, unknown>, shape?.pathOf?.(_path) ?? _path, { ...(journey.headers ?? {}), ...(s.headers ?? {}) }, placeholderIds, (s as { path?: string }).path)) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `the request is not the example's published input: ${p}` });
1146
+ if (ex.restXml) {
1147
+ if (request) for (const p of restXmlInputProblems(ex.restXml, (input ?? {}) as Record<string, unknown>, shape?.pathOf(request.method, request.url, request.headers) ?? request.url, request.headers, placeholderIds, (s as { path?: string }).path)) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `the request is not the example's published input: ${p}` });
1148
+ }
1135
1149
  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)))) 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" });
1136
1150
  // a scenario's turn is sent as the RFC writes it: its client lines (none for the greeting, a connect)
1137
1151
  if (ex.lines && JSON.stringify([s.send ?? []].flat()) !== JSON.stringify(ex.lines)) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `sends ${JSON.stringify(s.send ?? null)}, not the scenario's ${JSON.stringify(ex.lines)}` });
1138
- const target = Object.entries({ ...journey.headers, ...s.headers }).find(([k]) => k.toLowerCase() === 'x-amz-target')?.[1];
1152
+ const target = request ? new Headers(request.headers).get('x-amz-target') : undefined;
1139
1153
  if (ex.target && !target?.endsWith(`.${ex.target}`)) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `x-amz-target is ${target}, not the example's operation ${ex.target}` });
1140
1154
  if (ex.status && answer.status !== ex.status && !s.exampleDiffers?.status) failures.push({ step: s.name, beat: 'EXAMPLE', detail: `answered ${answer.status}, the example ${ex.status}` });
1141
1155
  if (stepFailures.length) return;
@@ -1166,9 +1180,8 @@ await walk(journey, world.root(), failures, {
1166
1180
  const published = isResp(s) && publishedValue !== undefined ? (Array.isArray(s.resp?.[0]) ? publishedValue : [publishedValue]) : publishedValue;
1167
1181
  const responseType = answer.headers?.['content-type'];
1168
1182
  const literal = isResp(s) || (published !== null && typeof published !== 'object');
1169
- const valueSchema = ex.schema ?? operationSchemas[ex.operation]?.responses[String(answer.status)];
1170
- comparisonCollections = [...(storedCollectionFields[vendor]?.[ex.operation] ?? [])];
1171
- const operation = shape?.operationOf((s.method ?? 'GET').toUpperCase(), _path, { ...(journey.headers ?? {}), ...(s.headers ?? {}) })?.op;
1183
+ const valueSchema = ex.schema ?? operationSchemas[key]?.responses[String(answer.status)];
1184
+ comparisonCollections = [...(storedCollectionFields[vendor]?.[key] ?? [])];
1172
1185
  // A derived resource list names its holder; never treat every array under a read as a list.
1173
1186
  if (operation?.class === 'list' && operation.answers?.list) {
1174
1187
  const holder = operation.answers.key ? (published as Record<string, unknown>)?.[operation.answers.key] : published;
@@ -1209,7 +1222,7 @@ await walk(journey, world.root(), failures, {
1209
1222
  // an example of an operation the twin does not serve is breadth's gap (score-pack), counted here and not replayed
1210
1223
  type Owned = { owners?: () => Record<string, string> } | undefined;
1211
1224
  const owners = ((await vendorFetch(PACKS, vendor))?.(world.root()) as Owned)?.owners?.() ?? ((await vendorStream(PACKS, vendor))?.(world.root()) as Owned)?.owners?.() ?? {};
1212
- const ownerOf = (e: Example): string | undefined => e.wire === 'client' ? 'gap' : owners[e.lane ? `${e.lane}:${e.operation}` : e.operation];
1225
+ const ownerOf = (e: Example): string | undefined => e.wire === 'client' ? 'gap' : owners[operationKey(e.operation, e.lane)];
1213
1226
  const patchRemoved = examples.filter((e) => e.patchRemoved);
1214
1227
  const unserved = examples.filter((e) => !e.patchRemoved && ownerOf(e) === 'gap');
1215
1228
  const recorded = Object.keys(journey.notReplayed ?? {});