@volter/twin-standard 1.0.158 → 1.0.160

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 (100) 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/check-sources.js +2 -2
  10. package/dist/src/clients/harness.d.ts +25 -4
  11. package/dist/src/clients/harness.js +184 -22
  12. package/dist/src/clients/jira.js +11 -2
  13. package/dist/src/clients/resend.first-use.json +2 -2
  14. package/dist/src/clients/run.js +19 -14
  15. package/dist/src/conformance.d.ts +3 -4
  16. package/dist/src/conformance.js +396 -203
  17. package/dist/src/consumer.d.ts +22 -1
  18. package/dist/src/consumer.js +91 -26
  19. package/dist/src/create-pack.d.ts +1 -1
  20. package/dist/src/create-pack.js +13 -12
  21. package/dist/src/credential-issuer-import.d.ts +3 -0
  22. package/dist/src/credential-issuer-import.js +42 -0
  23. package/dist/src/derive.js +6 -3
  24. package/dist/src/gate.js +4 -0
  25. package/dist/src/index.d.ts +1 -1
  26. package/dist/src/index.js +1 -1
  27. package/dist/src/lanes.d.ts +2 -0
  28. package/dist/src/lanes.js +7 -0
  29. package/dist/src/pack-facts.d.ts +3 -0
  30. package/dist/src/pack-facts.js +36 -1
  31. package/dist/src/protocol-3.js +13 -8
  32. package/dist/src/spec-documents.d.ts +4 -0
  33. package/dist/src/spec-documents.js +110 -3
  34. package/dist/src/spec-ir-client.js +1 -1
  35. package/dist/src/spec-ir-commands.js +1 -1
  36. package/dist/src/spec-ir-lines.js +1 -1
  37. package/dist/src/spec-ir-proto.d.ts +1 -1
  38. package/dist/src/spec-patches.js +2 -2
  39. package/dist/src/tools/judge-brief.js +1 -2
  40. package/dist/src/tools/life-coverage-walk.js +38 -26
  41. package/dist/src/tools/life-coverage.js +2 -2
  42. package/dist/src/tools/packs-root.js +3 -3
  43. package/dist/src/tools/score-pack.js +7 -7
  44. package/dist/src/tools/vendor-examples.js +95 -85
  45. package/dist/src/verdict.js +1 -1
  46. package/dist/src/walk/behavior-journey.d.ts +22 -14
  47. package/dist/src/walk/behavior-journey.js +199 -133
  48. package/dist/src/walk/journey-kit.d.ts +5 -4
  49. package/dist/src/walk/journey-kit.js +6 -2
  50. package/dist/src/walk/replay.d.ts +23 -0
  51. package/dist/src/walk/replay.js +118 -0
  52. package/dist/src/walk/serve-pack.d.ts +2 -2
  53. package/dist/src/walk/serve-pack.js +44 -2
  54. package/dist/src/walk/shape.d.ts +26 -5
  55. package/dist/src/walk/shape.js +167 -57
  56. package/dist/src/walk/vendor-fetch.d.ts +7 -2
  57. package/dist/src/walk/vendor-fetch.js +13 -2
  58. package/dist/src/walk/walk-database.d.ts +9 -1
  59. package/dist/src/walk/walk-database.js +35 -13
  60. package/dist/src/walk/xml-view.d.ts +3 -2
  61. package/dist/src/walk/xml-view.js +5 -4
  62. package/package.json +6 -4
  63. package/src/assessment.ts +12 -8
  64. package/src/browser-assessment.ts +12 -5
  65. package/src/check-sources.ts +2 -2
  66. package/src/clients/harness.ts +142 -19
  67. package/src/clients/jira.ts +4 -2
  68. package/src/clients/resend.first-use.json +2 -2
  69. package/src/clients/run.ts +17 -15
  70. package/src/conformance.ts +317 -147
  71. package/src/consumer.ts +83 -21
  72. package/src/create-pack.ts +13 -12
  73. package/src/credential-issuer-import.ts +38 -0
  74. package/src/derive.ts +6 -4
  75. package/src/gate.ts +3 -0
  76. package/src/index.ts +1 -1
  77. package/src/lanes.ts +7 -0
  78. package/src/pack-facts.ts +25 -2
  79. package/src/protocol-3.ts +12 -8
  80. package/src/spec-documents.ts +92 -3
  81. package/src/spec-ir-client.ts +1 -1
  82. package/src/spec-ir-commands.ts +1 -1
  83. package/src/spec-ir-lines.ts +1 -1
  84. package/src/spec-ir-proto.ts +1 -1
  85. package/src/spec-patches.ts +2 -2
  86. package/src/tools/judge-brief.ts +1 -2
  87. package/src/tools/life-coverage-walk.ts +32 -22
  88. package/src/tools/life-coverage.ts +2 -2
  89. package/src/tools/packs-root.ts +3 -3
  90. package/src/tools/score-pack.ts +8 -8
  91. package/src/tools/vendor-examples.ts +92 -80
  92. package/src/verdict.ts +1 -1
  93. package/src/walk/behavior-journey.ts +158 -109
  94. package/src/walk/journey-kit.ts +9 -7
  95. package/src/walk/replay.ts +103 -0
  96. package/src/walk/serve-pack.ts +30 -5
  97. package/src/walk/shape.ts +157 -55
  98. package/src/walk/vendor-fetch.ts +16 -4
  99. package/src/walk/walk-database.ts +41 -15
  100. package/src/walk/xml-view.ts +5 -4
@@ -43,8 +43,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
43
43
  // read for. `--sweep` prints, without changing done, every actor field (creator, user, *_by, ...) no `equals` pins and every
44
44
  // list no expect closes: advice for fixing a ruling as a class (its hits include lists a pinned text already fixes).
45
45
  //
46
- // Usage: twin-standard examples <vendor> [--sweep] (from the pack repository's root; twin-world's
47
- // bun scripts/vendor-examples.ts <vendor> runs the same)
46
+ // Usage: twin-standard examples <vendor> [--sweep] (from the pack repository's root)
48
47
  import { parseExactJson, exactInteger, isExactIntegerValue } from '@volter/world-core/exact-json';
49
48
  import { hasOwnSurface } from "../lanes.js";
50
49
  import { archiveSource } from "../archive-source.js";
@@ -56,7 +55,7 @@ import { basename, join } from 'node:path';
56
55
  import { gunzipSync } from 'node:zlib';
57
56
  import { inMemoryWorld, walk, usePacks } from "../walk/behavior-journey.js";
58
57
  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";
58
+ import { at as selectorAt, captureAt, expandFills, isResp, isGit, isSocket, isWait } from "../walk/journey-kit.js";
60
59
  import { lanesOf, shapeChecker } from "../walk/shape.js";
61
60
  import { applySpecPatch, discoveryToOpenAPI, mergeDocuments, readSpecDocuments, slug } from "../spec.js";
62
61
  import { packsRoot } from "./packs-root.js";
@@ -70,6 +69,7 @@ let specSchemas = {};
70
69
  const schemasByOperation = {};
71
70
  /** Each operation's request-body schema and its JSON answers' schemas by status, as the served spec gives them. */
72
71
  const operationSchemas = {};
72
+ const operationKey = (operation, lane) => lane ? `${lane}:${operation}` : operation;
73
73
  /** A page's placeholder id in an answer-map key is the id sent at the corresponding published request position.
74
74
  * Only declared placeholders bind, and an ambiguous repeated placeholder cannot stand for a guessed map key. */
75
75
  function answerKeys(output, published, sent, placeholder, rawBindings = false) {
@@ -94,7 +94,7 @@ function answerKeys(output, published, sent, placeholder, rawBindings = false) {
94
94
  : value;
95
95
  return mapped(output);
96
96
  }
97
- /** The schemas the twin serves: the vendor's, with the pack's spec patches (spec/patches.json) applied as derive-pack
97
+ /** The schemas the twin serves: the vendor's, with the pack's spec patches (spec/patches.json) applied as the derivation
98
98
  * applies them. A sample is checked against what the twin is derived from; the published examples themselves stay the
99
99
  * vendor's bytes. */
100
100
  function served(pkg, doc) {
@@ -106,7 +106,7 @@ function served(pkg, doc) {
106
106
  applySpecPatch(patched, p);
107
107
  return patched;
108
108
  }
109
- let xmlModel;
109
+ const restXmlService = (model) => Object.values(model.shapes).some((shape) => shape.type === 'service' && shape.traits?.['aws.protocols#restXml'] !== undefined);
110
110
  /** A translated placeholder-key map is a complete map even when its surrounding documented request is partial. */
111
111
  function requestMapKeysHold(published, translated, sent, placeholder) {
112
112
  if (!placeholder || !published || typeof published !== 'object')
@@ -124,11 +124,11 @@ function requestMapKeysHold(published, translated, sent, placeholder) {
124
124
  });
125
125
  }
126
126
  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) => {
127
+ const scalarType = (model, target) => model.shapes[target]?.type ?? PRELUDE[target.split('#')[1] ?? ''] ?? 'string';
128
+ const scalarOf = (model, target, v) => {
129
129
  if (typeof v !== 'string')
130
130
  return v;
131
- const t = scalarType(target);
131
+ const t = scalarType(model, target);
132
132
  if (t === 'boolean')
133
133
  return v === 'true';
134
134
  if (['integer', 'long', 'short', 'byte'].includes(t))
@@ -138,10 +138,10 @@ const scalarOf = (target, v) => {
138
138
  const arrayOf = (v) => (v === undefined || v === '' ? [] : Array.isArray(v) ? v : [v]);
139
139
  /** An answer's XML members (xml-view) read by the model's shape: each member under its xmlName, a flattened list the
140
140
  * 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];
141
+ function typedXml(model, target, v) {
142
+ const shape = model.shapes[target];
143
143
  if (!shape || shape.type !== 'structure')
144
- return scalarOf(target, v);
144
+ return scalarOf(model, target, v);
145
145
  if (!v || typeof v !== 'object')
146
146
  return v === '' ? {} : v;
147
147
  const obj = v;
@@ -150,38 +150,38 @@ function typedXml(target, v) {
150
150
  if (Object.keys(m.traits ?? {}).some((t) => /^smithy\.api#http(Header|PrefixHeaders|ResponseCode)$/.test(t)))
151
151
  continue;
152
152
  const el = String(m.traits?.['smithy.api#xmlName'] ?? name);
153
- const ts = xmlModel.shapes[m.target];
153
+ const ts = model.shapes[m.target];
154
154
  const raw = obj[el];
155
155
  if (ts?.type === 'list') {
156
156
  if (m.traits?.['smithy.api#xmlFlattened'] !== undefined) {
157
157
  if (raw !== undefined)
158
- out[name] = arrayOf(raw).map((i) => typedXml(ts.member.target, i));
158
+ out[name] = arrayOf(raw).map((i) => typedXml(model, ts.member.target, i));
159
159
  continue;
160
160
  }
161
161
  if (raw === undefined)
162
162
  continue;
163
163
  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));
164
+ out[name] = arrayOf(raw === '' ? undefined : raw[memberEl]).map((i) => typedXml(model, ts.member.target, i));
165
165
  }
166
166
  else if (raw !== undefined)
167
- out[name] = typedXml(m.target, raw);
167
+ out[name] = typedXml(model, m.target, raw);
168
168
  }
169
169
  return out;
170
170
  }
171
171
  /** A restXml operation's success answer as its output structure: the body's members by shape (or its payload
172
172
  * member's), and the members its headers carry. */
173
- function restXmlAnswer(opId, body, headers = {}) {
174
- const out = xmlModel.shapes[xmlModel.shapes[opId]?.output?.target ?? ''];
173
+ function restXmlAnswer({ id: opId, model }, body, headers = {}) {
174
+ const out = model.shapes[model.shapes[opId]?.output?.target ?? ''];
175
175
  if (!out)
176
176
  return body;
177
177
  const members = Object.entries((out.members ?? {}));
178
178
  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));
179
+ const answer = (payload ? { [payload[0]]: typedXml(model, payload[1].target, body) } : typedXml(model, model.shapes[opId].output.target, body));
180
180
  const lower = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]));
181
181
  for (const [name, m] of members) {
182
182
  const h = m.traits?.['smithy.api#httpHeader'];
183
183
  if (h !== undefined && lower[String(h).toLowerCase()] !== undefined)
184
- answer[name] = scalarOf(m.target, lower[String(h).toLowerCase()]);
184
+ answer[name] = scalarOf(model, m.target, lower[String(h).toLowerCase()]);
185
185
  // a map bound to a header prefix (S3's Metadata, `x-amz-meta-*`): every header under it, by the rest of its name
186
186
  const prefix = m.traits?.['smithy.api#httpPrefixHeaders'];
187
187
  if (prefix !== undefined)
@@ -191,8 +191,8 @@ function restXmlAnswer(opId, body, headers = {}) {
191
191
  }
192
192
  /** Where a restXml example's input is not what the step sends: each member the example gives, at its binding (a path
193
193
  * 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 ?? ''];
194
+ function restXmlInputProblems({ id: opId, model }, input, path, headers, placeholder, rawPath) {
195
+ const shape = model.shapes[model.shapes[opId]?.input?.target ?? ''];
196
196
  const url = new URL(path, 'http://twin.local');
197
197
  // 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
198
  // UploadId), which stands for the example's literal as a spec's ids stand for the World's (architecture, "Published examples")
@@ -236,11 +236,13 @@ function lenientJson(text) {
236
236
  * response's `example`/`examples` (a `$ref` to `components.examples` followed), a Swagger 2 response's `examples`, Stripe's
237
237
  * 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
238
  * kept as text and compared with the published framing and payload). */
239
- function published(pkg) {
239
+ function published(pkg, lane) {
240
240
  // Both the vendor API and its lanes publish examples; a root spec must not hide a lane. A lane's example names its
241
241
  // 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) })))];
242
+ const smithy = ['smithy.json.gz', 'smithy.json'].map((name) => join(pkg, 'spec', name)).find(existsSync);
243
+ const model = smithy ? parseExactJson((smithy.endsWith('.gz') ? gunzipSync(readFileSync(smithy)) : readFileSync(smithy)).toString('utf8')) : undefined;
244
+ const own = [...(hasOwnSurface(pkg) ? fromSpec(pkg, lane, model) : []), ...fromDocs(pkg, lane, model)].map((e) => ({ ...e, lane }));
245
+ return [...own, ...lanesOf(pkg).flatMap((dir) => published(dir, lane ? `${lane}:${basename(dir)}` : basename(dir)))];
244
246
  }
245
247
  /** The examples a vendor's documentation pages publish beside its spec (a schedule page's expressions, each with what
246
248
  * it means), vendored in `spec/doc-examples.json` as `[{title, source, operation, request?, input?, output?, means, status?}]`:
@@ -251,7 +253,7 @@ function published(pkg) {
251
253
  * reference writes every property, and every scalar as a string, `"verified": "true"` beside a schema saying boolean)
252
254
  * shows what the answer can hold, not one request's answer: its example is a sample against the served schema, and a
253
255
  * 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) {
256
+ function fromDocs(pkg, lane, model) {
255
257
  const file = join(pkg, 'spec', 'doc-examples.json');
256
258
  if (!existsSync(file))
257
259
  return [];
@@ -294,12 +296,11 @@ function fromDocs(pkg) {
294
296
  for (const d of docs)
295
297
  if (d.placeholders && (!d.placeholders.names?.length || !d.placeholders.quote?.trim()))
296
298
  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
299
  // a page's example is a fragment (an expression, not the request that carries it): the body holds it
299
300
  return docs.map((d) => {
300
301
  if (!d.rendered)
301
302
  return fromPage(d, d.output, d.input);
302
- const schemas = operationSchemas[d.operation];
303
+ const schemas = operationSchemas[operationKey(d.operation, lane)];
303
304
  const schema = schemas?.responses[String(d.status ?? 200)];
304
305
  if (!schema)
305
306
  throw new Error(`${file}: "${d.title}" is rendered from a schema the served spec does not give (${d.operation}, ${d.status ?? 200})`);
@@ -311,7 +312,14 @@ function fromDocs(pkg) {
311
312
  function fromPage(d, output, input) {
312
313
  const named = d.placeholders?.names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
313
314
  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 } : {}) };
315
+ let restXml;
316
+ if (model && restXmlService(model)) {
317
+ const id = Object.keys(model.shapes).find((key) => key.endsWith(`#${d.operation}`));
318
+ if (!id)
319
+ throw new Error(`${file}: "${d.title}" names no operation in its XML service (${d.operation})`);
320
+ restXml = { id, model };
321
+ }
322
+ 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
323
  }
316
324
  }
317
325
  /** A rendered object the page does not fill: any object stands where it is. */
@@ -400,12 +408,25 @@ function decodedExample(media, value) {
400
408
  return value;
401
409
  }
402
410
  }
403
- function fromSpec(pkg) {
411
+ function fromSpec(pkg, lane, model) {
404
412
  const rfc = existsSync(join(pkg, 'spec')) ? readdirSync(join(pkg, 'spec')).find((f) => /^rfc\d+\.txt$/.test(f)) : undefined;
405
413
  if (rfc)
406
414
  return scenarios(readFileSync(join(pkg, 'spec', rfc), 'utf8'));
407
- // 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));
415
+ const out = [];
416
+ if (model) {
417
+ const restXml = restXmlService(model);
418
+ for (const [id, shape] of Object.entries(model.shapes)) {
419
+ const examples = shape.traits?.['smithy.api#examples'];
420
+ const operation = id.split('#')[1];
421
+ // an awsJson operation is named by its x-amz-target header, `<service>.<operation>`; its body is the input. A
422
+ // restXml operation (S3's) is named by its method and path, and its input is where each member's binding puts it
423
+ for (const e of examples ?? [])
424
+ out.push({ id: `${operation}#${e.title}`, operation, output: e.error ? e.error.content : e.output, input: e.input ?? {}, ...(restXml ? { restXml: { id, model } } : { target: operation }) });
425
+ }
426
+ return out;
427
+ }
428
+ // a spec vendored as published or gzipped, as the derivation reads it
429
+ 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
430
  // a Protocol Buffers service publishes no examples in its .proto files: its examples are its documentation's alone
410
431
  if (!spec && existsSync(join(pkg, 'spec', 'proto')))
411
432
  return [];
@@ -420,23 +441,7 @@ function fromSpec(pkg) {
420
441
  if (!spec && !documents)
421
442
  throw new Error(`${pkg}: no published-example reader for this spec yet`);
422
443
  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
- // a Google API Discovery document publishes no examples; it is read as the OpenAPI document derive-pack reads it as,
444
+ // a Google API Discovery document publishes no examples; it is read as the OpenAPI document the derivation reads it as,
440
445
  // its patches applied to the Discovery document first (spec-ir-discovery.ts)
441
446
  const discovery = spec !== undefined && /discovery\.json(\.gz)?$/.test(spec);
442
447
  const docs = (documents ? Object.values(documents.documents) : [discovery ? discoveryToOpenAPI(parseExactJson(text)) : /\.yaml(\.gz)?$/.test(spec) ? parseExactJson(JSON.stringify(parseYaml(text, { intAsBigInt: true }), (_key, value) => typeof value === 'bigint' ? exactInteger(String(value)) : value)) : parseExactJson(text)]);
@@ -454,8 +459,9 @@ function fromSpec(pkg) {
454
459
  continue;
455
460
  // Swagger 2: the request is the `in: body` parameter's schema, an answer its response's `schema`
456
461
  const bodyParam = (op.parameters ?? []).find((x) => x.in === 'body');
457
- schemasByOperation[idOf(op, method, path)] = specSchemas;
458
- operationSchemas[idOf(op, method, path)] = {
462
+ const key = operationKey(idOf(op, method, path), lane);
463
+ schemasByOperation[key] = specSchemas;
464
+ operationSchemas[key] = {
459
465
  request: json(op.requestBody?.content) ?? bodyParam?.schema,
460
466
  responses: Object.fromEntries(Object.entries(op.responses ?? {}).flatMap(([code, r]) => { const sch = json(r.content) ?? r.schema; return sch ? [[code, sch]] : []; })),
461
467
  };
@@ -978,17 +984,17 @@ function outputSchemaMismatches(schema, value, prefix, depth = 0) {
978
984
  * recorded with the per-example classification. Request echoes and generated output are absent. */
979
985
  const storedCollectionFields = {
980
986
  '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'] },
987
+ aws: { 's3:ListBuckets': ['Buckets'], 's3:ListMultipartUploads': ['Uploads'], 's3:ListObjects': ['Contents', 'CommonPrefixes'], 's3:ListObjectsV2': ['Contents', 'CommonPrefixes'] },
988
+ cloudflare: { 'r2:ListBuckets': ['Buckets'], 'r2:ListMultipartUploads': ['Uploads'], 'r2:ListObjects': ['Contents', 'CommonPrefixes'], 'r2:ListObjectsV2': ['Contents', 'CommonPrefixes'] },
983
989
  discord: { oauth2_token: ['guild.roles'], get_my_oauth2_authorization: ['scopes'] },
984
990
  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: [''] },
991
+ hackernews: { 'api:getItem': ['kids'], 'api:getUser': ['submitted'], 'api:getUpdates': ['items', 'profiles'], 'api:getTopStories': [''], 'api:getNewStories': [''], 'api:getBestStories': [''], 'api:getAskStories': [''], 'api:getShowStories': [''], 'api:getJobStories': [''] },
986
992
  linkedin: { findOrganizationAclsByRoleAssignee: ['elements'] },
987
993
  'npm-registry': { 'libnpmsearch.searchStream': ['objects', 'objects.[].package.maintainers'] },
988
994
  openrouter: { getCurrentKey: ['data.allowed_data_regions'], getModels: ['data'] },
989
995
  redis: { SCAN: ['[].1'] },
990
996
  resend: { 'domains/get': ['records'], 'emails/list-receiving': ['data', 'data.[].attachments'] },
991
- sentry: { listOrganizations: ['', '[].features'] },
997
+ sentry: { 'api:listOrganizations': ['', '[].features'] },
992
998
  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
999
  tinybird: { datasources: ['datasources'], jobs: ['jobs'], pipes: ['pipes'], tokens: ['tokens'], token_get: ['scopes'], token_refresh: ['scopes'], pipe_set_endpoint: ['nodes'] },
994
1000
  };
@@ -1104,7 +1110,7 @@ if (!vendor || vendor.startsWith('-')) {
1104
1110
  const PKG = join(PACKS, vendor);
1105
1111
  const file = join(PKG, 'journeys', 'vendor-examples.json');
1106
1112
  await loadPlaceholders(PKG);
1107
- const examples = published(PKG);
1113
+ const examples = published(PKG, undefined);
1108
1114
  const byId = new Map(examples.map((e) => [e.id, e]));
1109
1115
  if (byId.size !== examples.length)
1110
1116
  throw new Error(`${vendor}: ${examples.length - byId.size} published examples share an id`);
@@ -1121,11 +1127,13 @@ const shape = await shapeChecker(PKG);
1121
1127
  for (const e of examples) {
1122
1128
  if (e.patchRemoved || !e.request || !shape)
1123
1129
  continue;
1124
- const routed = shape.operationOf(e.request.method, e.request.url, {})?.op.id;
1130
+ const hit = shape.operationOf(e.request.method, new URL(e.request.url, `http://${journey.host ?? 'twin.local'}`).href, e.request.headers);
1131
+ const routed = hit ? operationKey(hit.op.id, hit.lane) : undefined;
1125
1132
  if (e.operation === 'gap' && routed === undefined)
1126
1133
  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}`);
1134
+ const expected = operationKey(e.operation, e.lane);
1135
+ if (routed !== expected)
1136
+ 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
1137
  }
1130
1138
  const sweeping = process.argv.includes('--sweep');
1131
1139
  const sweepHits = [];
@@ -1258,18 +1266,20 @@ await walk(journey, world.root(), failures, {
1258
1266
  after: (step, _path, answer, stepFailures) => {
1259
1267
  if (isGit(step) || isWait(step) || isSocket(step))
1260
1268
  return;
1261
- if (!isResp(step) && !isLine(step)) {
1269
+ const request = answer.request;
1270
+ const hit = request ? shape?.operationOf(request.method, request.url, request.headers) : undefined;
1271
+ const operation = hit?.op;
1272
+ if (request) {
1262
1273
  const st = step;
1263
- const method = (st.method ?? 'GET').toUpperCase();
1274
+ const method = request.method;
1264
1275
  // 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;
1276
+ const opClass = operation?.class;
1267
1277
  const isRead = opClass ? ['retrieve', 'list', 'computed'].includes(opClass) : method === 'GET';
1268
1278
  if (isRead)
1269
- reads.push({ path: bare(_path), body: JSON.stringify(answer.body ?? '') });
1279
+ reads.push({ path: bare(request.url), body: JSON.stringify(answer.body ?? '') });
1270
1280
  else if (st.example && answer.status < 300 && answer.body && typeof answer.body === 'object' && !Array.isArray(answer.body) && Object.keys(answer.body).length <= 3) {
1271
1281
  const body = answer.body;
1272
- const segs = bare(_path).split('/');
1282
+ const segs = bare(request.url).split('/');
1273
1283
  // an action on a subject (`/emails/{id}/cancel`) touches the subject; a create touches the new id under the collection;
1274
1284
  // 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
1285
  const minted = typeof body.id === 'string' ? body.id : '';
@@ -1277,8 +1287,7 @@ await walk(journey, world.root(), failures, {
1277
1287
  if (minted || idInPath) {
1278
1288
  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
1289
  // 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';
1290
+ const gets = (at) => shape?.operationOf('GET', new URL(at, request.url).href, request.headers)?.op.method.toUpperCase() === 'GET';
1282
1291
  const door = !shape || gets(base) || gets(base.split('/').slice(0, -1).join('/'));
1283
1292
  if (door)
1284
1293
  thinWrites.push({ step: st.name, base, id: base.split('/').at(-1) ?? '', from: reads.length, deletes: method === 'DELETE' || opClass === 'delete' });
@@ -1308,20 +1317,20 @@ await walk(journey, world.root(), failures, {
1308
1317
  unknown.push(s.example);
1309
1318
  return;
1310
1319
  }
1311
- specSchemas = schemasByOperation[ex.operation] ?? {};
1320
+ const key = operationKey(ex.operation, ex.lane);
1321
+ specSchemas = schemasByOperation[key] ?? {};
1312
1322
  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)
1323
+ if (ex.operation === 'gap' && ex.request && request) {
1324
+ if (request.method !== ex.request.method || request.url !== ex.request.url)
1316
1325
  failures.push({ step: s.name, beat: 'EXAMPLE', detail: 'the unknown request differs from its recorded method or URL' });
1317
1326
  }
1318
- if (ex.authorizationAbsent && Object.keys({ ...(journey.headers ?? {}), ...(s.headers ?? {}) }).some((key) => key.toLowerCase() === 'authorization'))
1327
+ if (ex.authorizationAbsent && request && new Headers(request.headers).has('authorization'))
1319
1328
  failures.push({ step: s.name, beat: 'EXAMPLE', detail: 'the recording has no Authorization header, but the replay sends one' });
1320
1329
  // 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}` });
1330
+ if (request && shape) {
1331
+ const called = hit ? operationKey(hit.op.id, hit.lane) : undefined;
1332
+ if (called && called !== key)
1333
+ failures.push({ step: s.name, beat: 'EXAMPLE', detail: `calls ${called}, not the example's operation ${key}` });
1325
1334
  }
1326
1335
  // the example is sent as published: its input as the body, to its operation
1327
1336
  // a published field the step leaves out (`unsent`: a rendered value naming an object the World does not hold) must
@@ -1329,26 +1338,28 @@ await walk(journey, world.root(), failures, {
1329
1338
  for (const [path, why] of Object.entries(s.unsent ?? {})) {
1330
1339
  if (!why)
1331
1340
  failures.push({ step: s.name, beat: 'EXAMPLE', detail: `unsent ${path} gives no reason` });
1332
- if (requiredAt(operationSchemas[ex.operation]?.request, path))
1341
+ if (requiredAt(operationSchemas[key]?.request, path))
1333
1342
  failures.push({ step: s.name, beat: 'EXAMPLE', detail: `unsent ${path} is required by the operation's request schema` });
1334
1343
  }
1335
1344
  const publishedBody = exampleBody({ ...s, body: ex.input, form: undefined, bodyBase64: undefined }, ex.input);
1336
1345
  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];
1346
+ ?? (request ? new Headers(request.headers).get('content-type') ?? undefined : undefined);
1338
1347
  const publishedInput = s.unsent && publishedBody !== undefined ? withoutPaths(publishedBody, Object.keys(s.unsent)) : publishedBody;
1339
1348
  // 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
1349
  const body = isResp(s) ? s.resp : exampleBody(s, publishedInput) ?? {};
1341
1350
  // A placeholder used as a request-map key is bound by its scalar request position, just as an answer key is.
1342
1351
  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}` });
1352
+ if (ex.restXml) {
1353
+ if (request)
1354
+ for (const p of restXmlInputProblems(ex.restXml, (input ?? {}), shape?.pathOf(request.method, request.url, request.headers) ?? request.url, request.headers, placeholderIds, s.path))
1355
+ failures.push({ step: s.name, beat: 'EXAMPLE', detail: `the request is not the example's published input: ${p}` });
1356
+ }
1346
1357
  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
1358
  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
1359
  // a scenario's turn is sent as the RFC writes it: its client lines (none for the greeting, a connect)
1349
1360
  if (ex.lines && JSON.stringify([s.send ?? []].flat()) !== JSON.stringify(ex.lines))
1350
1361
  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];
1362
+ const target = request ? new Headers(request.headers).get('x-amz-target') : undefined;
1352
1363
  if (ex.target && !target?.endsWith(`.${ex.target}`))
1353
1364
  failures.push({ step: s.name, beat: 'EXAMPLE', detail: `x-amz-target is ${target}, not the example's operation ${ex.target}` });
1354
1365
  if (ex.status && answer.status !== ex.status && !s.exampleDiffers?.status)
@@ -1391,9 +1402,8 @@ await walk(journey, world.root(), failures, {
1391
1402
  const published = isResp(s) && publishedValue !== undefined ? (Array.isArray(s.resp?.[0]) ? publishedValue : [publishedValue]) : publishedValue;
1392
1403
  const responseType = answer.headers?.['content-type'];
1393
1404
  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;
1405
+ const valueSchema = ex.schema ?? operationSchemas[key]?.responses[String(answer.status)];
1406
+ comparisonCollections = [...(storedCollectionFields[vendor]?.[key] ?? [])];
1397
1407
  // A derived resource list names its holder; never treat every array under a read as a list.
1398
1408
  if (operation?.class === 'list' && operation.answers?.list) {
1399
1409
  const holder = operation.answers.key ? published?.[operation.answers.key] : published;
@@ -1439,7 +1449,7 @@ await walk(journey, world.root(), failures, {
1439
1449
  },
1440
1450
  });
1441
1451
  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];
1452
+ const ownerOf = (e) => e.wire === 'client' ? 'gap' : owners[operationKey(e.operation, e.lane)];
1443
1453
  const patchRemoved = examples.filter((e) => e.patchRemoved);
1444
1454
  const unserved = examples.filter((e) => !e.patchRemoved && ownerOf(e) === 'gap');
1445
1455
  const recorded = Object.keys(journey.notReplayed ?? {});
@@ -1,7 +1,7 @@
1
1
  // A JUDGE'S INDEPENDENCE (the grade's outline-judged and life-judged, the published examples' verdict): a verdict rules
2
2
  // only when it names its judge (`judge.agent`) and the authors of what it judges (`authors`), and the judge is none of
3
3
  // them. The dispatcher stamps author names consistently; a judge started independently
4
- // (scripts/judge-brief.ts prints its brief) names its reviewer role (`judge.name`). One judging per pack: the
4
+ // (`twin-standard judge-brief` prints its brief) names its reviewer role (`judge.name`). One judging per pack: the
5
5
  // verdict stands, and later changes to what it judged keep it (architecture, "Writing a life", Judge).
6
6
  /** Committed attribution names people or author roles, never execution identities. */
7
7
  export function attributionName(name) {
@@ -1,5 +1,8 @@
1
1
  import { type TwinStream } from '@volter/world-core/runtime';
2
2
  import { type Captured, type GitStep, type Journey, type JourneyStep, type LineStep, type RespStep } from './journey-kit.js';
3
+ import { type ReplayStep } from './replay.js';
4
+ import type { ManagedDatabaseWalk } from './walk-database.js';
5
+ export { leaked } from './shape.js';
3
6
  export declare function usePacks(dir: string, options?: {
4
7
  dependenciesFrom?: string;
5
8
  }): void;
@@ -13,26 +16,31 @@ export declare function inMemoryWorld(label: string): {
13
16
  root: () => string;
14
17
  restore: () => void;
15
18
  };
19
+ type SentRequest = {
20
+ method: string;
21
+ url: string;
22
+ headers: Record<string, string>;
23
+ };
24
+ type StepAnswer = {
25
+ status: number;
26
+ body: unknown;
27
+ headers?: Record<string, string>;
28
+ request?: SentRequest;
29
+ wireBody?: unknown;
30
+ sql?: ReplayStep['sql'];
31
+ connection?: ReplayStep['connection'];
32
+ };
16
33
  export type Failure = {
17
34
  step: string;
18
35
  beat: string;
19
36
  detail: string;
20
37
  };
21
- /** What a stored row keeps for the twin that no schema of the vendor's spec names, by path, in anything the World shows a
22
- * client: the pack's `_`-prefixed bookkeeping (GitHub's `_links` is the vendor's), and, in what no answer's SHAPE
23
- * reads (an event's delivery, a socket frame), the kernel's own `updatedAt`, which a raw row filtered on `_` alone
24
- * carries (Slack's message events once did). The kernel's `deleted` is not read there: vendors send their own
25
- * (Stripe's deleted customer's `deleted: true`). */
26
- export declare function leaked(v: unknown, vendorFields: Set<string>, at?: string, unshaped?: boolean): string[];
27
38
  /** What a caller that scores a walk observes around each step: the step, its resolved path and body,
28
- * and (after) the answer and the failures the step produced. */
39
+ * and (after) the answer and the failures the step produced. HTTP answers include the constructed request and
40
+ * decoded wire body separately from the capture view. */
29
41
  export type StepHooks = {
30
42
  before?: (step: JourneyStep | LineStep | GitStep | RespStep, path: string, body: unknown) => void;
31
- after?: (step: JourneyStep | LineStep | GitStep | RespStep, path: string, answer: {
32
- status: number;
33
- body: unknown;
34
- headers?: Record<string, string>;
35
- }, failures: Failure[]) => void;
43
+ after?: (step: JourneyStep | LineStep | GitStep | RespStep, path: string, answer: StepAnswer, failures: Failure[]) => void;
36
44
  };
37
45
  /** A pack served for a walk the way a World runs it (its server.ts on a port, the World's root and scenario its options):
38
46
  * the walk's requests go to it over HTTP, the vendor's host in their Host header, instead of to the fetch in process;
@@ -42,9 +50,9 @@ export type ServedTwin = (root: string, options: {
42
50
  application?: import('@volter/world-core').PackFetchOptions['application'];
43
51
  }) => Promise<{
44
52
  scheme?: 'http' | 'https';
45
- fetch: (request: Request, options?: {
53
+ fetch: ((request: Request, options?: {
46
54
  credentials?: RequestCredentials;
47
- }) => Promise<Response>;
55
+ }) => Promise<Response>) & ManagedDatabaseWalk;
48
56
  stop: () => void | Promise<void>;
49
57
  socket?: (url: string, headers: Record<string, string>) => WebSocket;
50
58
  stream?: TwinStream;