@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
@@ -1,3 +1,11 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
1
9
  import { parseExactJson } from '@volter/world-core/exact-json';
2
10
  import { attributionName } from "../verdict.js";
3
11
  // THE WALK — a pack's life (journeys/customer-life.json) walked step by step against the pack's own fetch, on an
@@ -11,16 +19,18 @@ import { attributionName } from "../verdict.js";
11
19
  import { mongodb, mysql, redis } from '@volter/world-core';
12
20
  import { createRequire } from 'node:module';
13
21
  import { gzipSync } from 'node:zlib';
14
- import { parse, visit } from 'graphql';
15
22
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
16
23
  import { join } from 'node:path';
17
- import { databaseSession, getActivePackAssets, getActiveWorldStore, LocalPackAssets, MemoryBlobStore, MemoryWorldStore, setActiveBlobStore, setActivePackAssets, setActiveWorldStore, setDeliveryObserver, setInProcessTwins } from '@volter/world-core/runtime';
24
+ import { databaseSession, getActivePackAssets, getActiveWorldStore, LocalPackAssets, MemoryBlobStore, MemoryWorldStore, setActiveBlobStore, setActivePackAssets, setActiveWorldStore, setDeliveryObserver, setInProcessMailServers, setInProcessTwins, settleEventDeliveries, worldDoorMatcher, worldNow } from '@volter/world-core/runtime';
18
25
  import { createHostTable } from '@volter/world-core/host-routing';
19
26
  import packFactsDocument from '@volter/world-core/generated/pack-facts.json' with { type: 'json' };
20
27
  import { isMongo, isMysql, isResp, at, captureAt, echoFailures, expandFills, expectFailures, fill, fillJson, formBody, isGit, isLine, isSql, isSocket, isWait, jwtAuthorization, oauth1Authorization, presignedPath, sigv4Headers, waitMs } from "./journey-kit.js";
21
- import { lanesOf, lineShape, shapeChecker } from "./shape.js";
28
+ import { decodeResponse, leaked, lineShape, responseProblems, shapeChecker, vendorFieldsOf } from "./shape.js";
22
29
  import { walkPackDirectory } from "./pack-directory.js";
23
- import { xmlMembers } from "./xml-view.js";
30
+ import { unitsIn } from "../lanes.js";
31
+ import { openWebSocket, readHttpResponse } from "../clients/harness.js";
32
+ import { timestampOrderFailures } from "./replay.js";
33
+ export { leaked } from "./shape.js";
24
34
  /** The pack repository the walk finds a vendor's pack in, set by the caller before its first walk. */
25
35
  let packsRoot;
26
36
  /** Installed companions resolve from the selected pack, including its workspace-local dependencies. */
@@ -146,18 +156,6 @@ const claims = (keys, url) => {
146
156
  const u = new URL(url);
147
157
  return keys.some((key) => walkHosts[key](u.hostname, u.pathname));
148
158
  };
149
- /** 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
150
- * client: the pack's `_`-prefixed bookkeeping (GitHub's `_links` is the vendor's), and, in what no answer's SHAPE
151
- * reads (an event's delivery, a socket frame), the kernel's own `updatedAt`, which a raw row filtered on `_` alone
152
- * carries (Slack's message events once did). The kernel's `deleted` is not read there: vendors send their own
153
- * (Stripe's deleted customer's `deleted: true`). */
154
- export function leaked(v, vendorFields, at = '', unshaped = false) {
155
- if (Array.isArray(v))
156
- return v.flatMap((x, i) => leaked(x, vendorFields, `${at}[${i}]`, unshaped)).slice(0, 5);
157
- if (!v || typeof v !== 'object')
158
- return [];
159
- return Object.entries(v).flatMap(([k, x]) => ((k.startsWith('_') || (unshaped && k === 'updatedAt')) && !vendorFields.has(k) ? [`${at}${at ? '.' : ''}${k}`] : leaked(x, vendorFields, `${at}${at ? '.' : ''}${k}`, unshaped))).slice(0, 5);
160
- }
161
159
  /** Whether a tool_choice forbids every tool call (OpenAI's 'none', Anthropic's { type: 'none' }). */
162
160
  const forbidsTools = (choice) => choice === 'none' || (!!choice && typeof choice === 'object' && choice.type === 'none');
163
161
  /** The tool calls an answer or frames carry, in any vendor's form: a chat message's `tool_calls`, a Responses or
@@ -180,16 +178,9 @@ function toolCallsIn(v, out = []) {
180
178
  toolCallsIn(x, out);
181
179
  return out;
182
180
  }
183
- /** Every field name the vendor's spec gives any resource (the derived surface), per vendor. */
181
+ /** The selected vendor's shared schema and native-error inventory, loaded before walking. */
184
182
  const fieldsByVendor = new Map();
185
183
  function specFields(vendor) {
186
- if (!fieldsByVendor.has(vendor)) {
187
- // the pack's surface and each of its lanes' (a walk reaches a lane through the pack's router)
188
- const directory = directoryFor(vendor);
189
- const files = [directory, ...lanesOf(directory)].map((dir) => join(dir, 'src', 'generated', 'surface.gen.json')).filter((f) => existsSync(f));
190
- const surfaces = files.map((f) => parseExactJson(readFileSync(f, 'utf8')));
191
- fieldsByVendor.set(vendor, new Set(surfaces.flatMap((surface) => (surface.resources ?? []).flatMap((r) => r.fields.map((f) => f.name)))));
192
- }
193
184
  return fieldsByVendor.get(vendor);
194
185
  }
195
186
  /** The complete replies a connection has written since last read, leaving an unfinished one. */
@@ -231,6 +222,27 @@ export async function walk(journey, root, collect, opts) {
231
222
  collect.push({ step: '(setup)', beat: 'ADAPTER', detail: `no create<Name>Fetch adapter resolved for ${journey.vendor}` });
232
223
  return {};
233
224
  }
225
+ // what an earlier walk captured, when this one continues it (the branch round-trip sends a life's write again)
226
+ const captured = { ...(opts.captured ?? {}) };
227
+ // the application the vendors call is the life's stand-in, shared by this walk's packs: its answers, in order,
228
+ // by method and URL prefix, each request header it declares checked as sent; a question none takes is unreachable, as
229
+ // in a sealed World
230
+ const answers = [...(journey.application ?? [])];
231
+ // the step being walked, for what the stand-in and the delivery observer report
232
+ let stepNow = '(setup)';
233
+ const application = async (url, init) => {
234
+ const method = (init.method ?? 'GET').toUpperCase();
235
+ const i = answers.findIndex((a) => (a.method ?? 'POST').toUpperCase() === method && url.startsWith(a.url));
236
+ if (i < 0)
237
+ return 'unreachable';
238
+ const [a] = answers.splice(i, 1);
239
+ const sent = new Headers(init.headers);
240
+ for (const [name, value] of Object.entries(a.requestHeaders ?? {})) {
241
+ if (sent.get(name) !== fill(value, captured))
242
+ collect.push({ step: stepNow, beat: 'APPLICATION', detail: `the vendor request to ${url} did not carry the declared ${name} header` });
243
+ }
244
+ return { status: a.status, ...(a.headers ? { headers: a.headers } : {}), ...(a.bodyError === undefined ? {} : { bodyError: a.bodyError }), body: typeof a.body === 'string' ? a.body : a.body === undefined ? '' : JSON.stringify(a.body) };
245
+ };
234
246
  // the life's companions (`companions`): the packs its vendor calls, each over this walk's root, on the same World and
235
247
  // clock; reached in process by the vendor's ctx.vendorFetch and by a step to a URL their host rules claim
236
248
  const companions = [];
@@ -241,7 +253,36 @@ export async function walk(journey, root, collect, opts) {
241
253
  collect.push({ step: '(setup)', beat: 'ADAPTER', detail: vendor === journey.vendor ? `the life's own vendor ${vendor} is no companion of it` : !companion ? `no create<Name>Fetch adapter for the companion ${vendor}` : `the companion ${vendor} declares no host rules (pack facts) a call could reach it by` });
242
254
  return {};
243
255
  }
244
- companions.push({ vendor, keys, fetch: companion(root) });
256
+ companions.push({ vendor, keys, fetch: companion(root, { application }), databaseDoor: companion.managedDatabaseDoor });
257
+ }
258
+ // a companion that receives mail (an SMTP side port: gmail's, gmail.com's MX) is the mail server ctx.mail reaches for
259
+ // the domains its host rules claim, on its byte-stream door in process over this walk's World, as a World reaches its
260
+ // listener: the life declares the provider of every address it mails
261
+ const mailServers = [];
262
+ for (const c of companions) {
263
+ const sides = walkFacts[c.vendor.split('/')[0]]?.sidePorts ?? [];
264
+ if (!sides.some((side) => side.scheme === 'smtp'))
265
+ continue;
266
+ const stream = await vendorStream(packsDir(), c.vendor, dependenciesDir());
267
+ if (!stream) {
268
+ collect.push({ step: '(setup)', beat: 'ADAPTER', detail: `the companion ${c.vendor} receives mail on an smtp side port and exports no create<Name>TwinStream` });
269
+ return {};
270
+ }
271
+ mailServers.push({ vendor: c.vendor, receives: (domain) => c.keys.some((key) => walkHosts[key](domain, '/')), stream: stream(root, { application }) });
272
+ }
273
+ const eventFields = new Map();
274
+ for (const vendor of [journey.vendor, ...companions.map((companion) => companion.vendor)]) {
275
+ const directory = directoryFor(vendor);
276
+ if (!fieldsByVendor.has(vendor))
277
+ fieldsByVendor.set(vendor, await vendorFieldsOf(directory));
278
+ for (const unit of unitsIn(directory)) {
279
+ const { manifest } = await import(__rewriteRelativeImportExtension(join(unit, 'src', 'manifest.ts')));
280
+ const service = manifest.service ?? manifest.vendor;
281
+ const fields = eventFields.get(service) ?? new Set();
282
+ for (const field of specFields(vendor))
283
+ fields.add(field);
284
+ eventFields.set(service, fields);
285
+ }
245
286
  }
246
287
  const ownKeys = routingKeys(journey.vendor);
247
288
  /** The companion that serves a step's URL: one whose host rules claim it where the life's own vendor's do not. */
@@ -251,32 +292,11 @@ export async function walk(journey, root, collect, opts) {
251
292
  const scenarioPath = journey.scenario ? join(directoryFor(journey.vendor), 'journeys', journey.scenario) : undefined;
252
293
  if (scenarioPath)
253
294
  getActiveWorldStore().write(scenarioPath, readFileSync(scenarioPath, 'utf8'));
254
- // the application the vendor asks (ctx.ask) is the life's stand-in, this walk's fetch's alone: its answers, in order,
255
- // by method and URL prefix, each request header it declares checked as sent; a question none takes is unreachable, as
256
- // in a sealed World
257
- const answers = [...(journey.application ?? [])];
258
- // the step being walked, for what the stand-in and the delivery observer report
259
- let stepNow = '(setup)';
260
- const application = async (url, init) => {
261
- const method = (init.method ?? 'GET').toUpperCase();
262
- const i = answers.findIndex((a) => (a.method ?? 'POST').toUpperCase() === method && url.startsWith(a.url));
263
- if (i < 0)
264
- return 'unreachable';
265
- const [a] = answers.splice(i, 1);
266
- const sent = new Headers(init.headers);
267
- for (const [name, value] of Object.entries(a.requestHeaders ?? {})) {
268
- if (sent.get(name) !== fill(value, captured))
269
- collect.push({ step: stepNow, beat: 'APPLICATION', detail: `the vendor request to ${url} did not carry the declared ${name} header` });
270
- }
271
- return { status: a.status, ...(a.headers ? { headers: a.headers } : {}), ...(a.bodyError === undefined ? {} : { bodyError: a.bodyError }), body: typeof a.body === 'string' ? a.body : a.body === undefined ? '' : JSON.stringify(a.body) };
272
- };
295
+ const shape = await shapeOf(journey.vendor);
273
296
  const served = opts.served ? await opts.served(root, { ...(scenarioPath ? { scenarioPath } : {}), application }) : undefined;
274
297
  const twin = served ? served.fetch : factory(root, { ...(scenarioPath ? { scenarioPath } : {}), application });
275
- const shape = await shapeOf(journey.vendor);
276
298
  const host = journey.host ?? 'twin.local';
277
299
  const origin = `${served?.scheme ?? 'http'}://${host}`;
278
- // what an earlier walk captured, when this one continues it (the branch round-trip sends a life's write again)
279
- const captured = { ...(opts.captured ?? {}) };
280
300
  // a capture's JSON value where it is not text (a number, a boolean, an object such as database-js's `session`)
281
301
  const typedValues = {};
282
302
  const remember = (name, v) => {
@@ -301,34 +321,27 @@ export async function walk(journey, root, collect, opts) {
301
321
  if (step.encode && typeof init.body === 'string')
302
322
  init.body = step.encode === 'gzip' ? new Uint8Array(gzipSync(Buffer.from(init.body))) : `data=${encodeURIComponent(Buffer.from(init.body).toString('base64'))}`;
303
323
  // a step may follow an absolute url an answer gave it (a hosted page's), as the application does
304
- // a twin that throws answers 500, as a server would: the walk reports the step and goes on to the next
305
- let res;
306
- try {
307
- const request = new Request(/^https?:\/\//.test(path) ? path : `${origin}${path}`, init);
308
- const companion = companionOf(path);
309
- res = companion ? await companion.fetch(request) : served ? await served.fetch(request, { credentials: step.credentials ?? 'same-origin' }) : await twin(request);
310
- }
311
- catch (error) {
312
- res = new Response(`twin threw: ${error instanceof Error ? error.message : String(error)}`, { status: 500 });
313
- }
314
- const text = await res.text();
315
- let body;
316
- try {
317
- body = parseExactJson(text);
318
- }
319
- catch {
320
- body = text;
321
- }
322
- // an XML answer (S3's) is its root element's members (xml-view.ts), as a JSON answer is its object
323
- if (typeof body === 'string' && /xml/.test(res.headers.get('content-type') ?? ''))
324
- body = xmlMembers(text) ?? text;
324
+ const companion = companionOf(path);
325
+ await (companion?.fetch ?? twin).managedDatabaseUrl?.();
326
+ const request = new Request(/^https?:\/\//.test(path) ? path : `${origin}${path}`, init);
327
+ const answer = await readHttpResponse(async (request) => {
328
+ // a twin that throws answers 500, as a server would: the walk reports the step and goes on to the next
329
+ try {
330
+ return companion ? await companion.fetch(request) : served ? await served.fetch(request, { credentials: step.credentials ?? 'same-origin' }) : await twin(request);
331
+ }
332
+ catch (error) {
333
+ if (served)
334
+ throw error;
335
+ return new Response(`twin threw: ${error instanceof Error ? error.message : String(error)}`, { status: 500 });
336
+ }
337
+ }, request);
338
+ const wireBody = decodeResponse(answer.text, answer.headers['content-type']);
339
+ let body = wireBody;
325
340
  // a hosted flow answers with a redirect: its target and query are what a step captures (redirect.code)
326
- const location = res.status >= 300 && res.status < 400 ? res.headers.get('location') : null;
341
+ const location = answer.status >= 300 && answer.status < 400 ? answer.headers.location : undefined;
327
342
  if (location)
328
343
  body = { location, redirect: Object.fromEntries(new URL(location, origin).searchParams) };
329
- // every cookie an answer sets, joined as one header: a sign-in sets several, and a map keeps only the last
330
- const cookies = res.headers.getSetCookie();
331
- return { status: res.status, body, text, headers: { ...Object.fromEntries(res.headers), ...(cookies.length ? { 'set-cookie': cookies.join(', ') } : {}) } };
344
+ return { status: answer.status, body, wireBody, request: { method: request.method, url: request.url, headers: Object.fromEntries(request.headers) }, text: answer.text, headers: answer.headers, ...(answer.bodyError ? { bodyError: answer.bodyError } : {}) };
332
345
  };
333
346
  // a wait moves the World clock; the walk puts it back where it found it, so a replay starts at the same instant
334
347
  // a step's read-backs are read as the step's own person (its headers, captures filled)
@@ -810,8 +823,7 @@ export async function walk(journey, root, collect, opts) {
810
823
  opened.frames.push(text);
811
824
  } };
812
825
  ws.onclose = (e) => { opened.closed ??= { code: e.code, reason: e.reason }; };
813
- const up = await new Promise((done) => { ws.onopen = () => done(true); ws.onerror = () => done(false); setTimeout(() => done(false), 5000); });
814
- if (!up) {
826
+ if (!await openWebSocket(ws)) {
815
827
  collect.push({ step: step.name, beat: 'STATUS', detail: `no socket at ${step.open}` });
816
828
  return;
817
829
  }
@@ -946,7 +958,8 @@ export async function walk(journey, root, collect, opts) {
946
958
  opts.hooks?.before?.(step, 'sql', step.sql);
947
959
  let answered = { status: 0, body: undefined };
948
960
  try {
949
- const url = await twin.managedDatabaseUrl?.();
961
+ const database = twin;
962
+ const url = await database.managedDatabaseUrl?.();
950
963
  if (url === undefined) {
951
964
  collect.push({ step: step.name, beat: 'ADAPTER', detail: `${journey.vendor} binds no managed database in this walk, so a SQL step has none to run on` });
952
965
  return;
@@ -964,9 +977,14 @@ export async function walk(journey, root, collect, opts) {
964
977
  collect.push({ step: step.name, beat: 'STATUS', detail: `Postgres refused it: ${code} ${e.message}${step.refused ? ` (wanted ${step.refused})` : ''}` });
965
978
  return;
966
979
  }
967
- const last = results.at(-1) ?? { fields: [], rows: [], command: '' };
980
+ const last = results.at(-1) ?? { fields: [], fieldTypes: [], rows: [], command: '' };
968
981
  const body = { command: last.command, rows: last.rows.map((row) => Object.fromEntries(last.fields.map((field, i) => [field, row[i] ?? null]))) };
969
- answered = { status: 0, body };
982
+ // Duplicate column names have the same last-column ownership as the visible row projection.
983
+ const fieldTypes = Object.fromEntries(last.fields.map((field, i) => [field, last.fieldTypes[i]]));
984
+ const timeFields = Object.fromEntries(Object.entries(fieldTypes).filter(([, type]) => type === 1114 || type === 1184));
985
+ answered = { status: 0, body, ...(database.managedDatabaseClock === 'wall' ? { sql: { clock: database.managedDatabaseClock, timeFields } } : {}) };
986
+ for (const detail of timestampOrderFailures(body, timeFields, step.timeOrder ?? []))
987
+ collect.push({ step: step.name, beat: 'EXPECT', detail });
970
988
  if (step.refused !== undefined) {
971
989
  collect.push({ step: step.name, beat: 'STATUS', detail: `Postgres completed it (${last.command}), wanted ${step.refused}` });
972
990
  return;
@@ -1015,15 +1033,22 @@ export async function walk(journey, root, collect, opts) {
1015
1033
  catch {
1016
1034
  return;
1017
1035
  }
1018
- for (const key of leaked(body, specFields(journey.vendor), '', true))
1036
+ const fields = eventFields.get(d.service);
1037
+ if (!fields) {
1038
+ collect.push({ step: stepNow, beat: 'LEAK', detail: `the event sent to ${d.url} names no selected owner for service ${d.service}` });
1039
+ return;
1040
+ }
1041
+ for (const key of leaked(body, fields, '', true))
1019
1042
  collect.push({ step: stepNow, beat: 'LEAK', detail: `the event sent to ${d.url} carries the twin's own field ${key}` });
1020
1043
  });
1021
1044
  const previousTwins = setInProcessTwins(Object.fromEntries([
1022
1045
  ...companions.flatMap((c) => c.keys.map((key) => [key, c.fetch])),
1023
1046
  ...ownKeys.map((key) => [key, twin]),
1024
1047
  ]));
1048
+ const previousMailServers = setInProcessMailServers(mailServers);
1025
1049
  try {
1026
1050
  for (const step of journey.steps) {
1051
+ await settleEventDeliveries(root);
1027
1052
  stepNow = step.name;
1028
1053
  if (isMongo(step)) {
1029
1054
  await mongoStep(step);
@@ -1059,7 +1084,7 @@ export async function walk(journey, root, collect, opts) {
1059
1084
  continue;
1060
1085
  }
1061
1086
  try {
1062
- store.write(clockFile, new Date(Date.parse((store.read(clockFile) ?? '').trim()) + waitMs(step.wait)).toISOString());
1087
+ store.write(clockFile, new Date(Date.parse(worldNow()) + waitMs(step.wait)).toISOString());
1063
1088
  }
1064
1089
  catch (e) {
1065
1090
  collect.push({ step: step.name, beat: 'WAIT', detail: e.message });
@@ -1103,8 +1128,7 @@ export async function walk(journey, root, collect, opts) {
1103
1128
  // a form is signed as it is sent: its multipart type, whose parts OAuth 1.0a's base string never carries
1104
1129
  const sent = formed ? undefined : body === undefined ? undefined : typeof body === 'string' ? body : JSON.stringify(body);
1105
1130
  const contentType = Object.entries(headers ?? {}).find(([k]) => k.toLowerCase() === 'content-type')?.[1] ?? (formed ? formed.contentType : step.bodyBase64 === undefined ? defaultType(body) : undefined);
1106
- const instant = clockFile && store.exists(clockFile) ? Date.parse((store.read(clockFile) ?? '').trim()) : Number.NaN;
1107
- headers = { ...(headers ?? {}), authorization: oauth1Authorization(o, step.method, url, sent, contentType, Number.isNaN(instant) ? 0 : instant) };
1131
+ headers = { ...(headers ?? {}), authorization: oauth1Authorization(o, step.method, url, sent, contentType, Date.parse(worldNow())) };
1108
1132
  }
1109
1133
  if (step.jwt)
1110
1134
  headers = { ...(headers ?? {}), authorization: jwtAuthorization(step.jwt, captured) };
@@ -1113,9 +1137,8 @@ export async function walk(journey, root, collect, opts) {
1113
1137
  const url = /^https?:\/\//.test(path) ? path : `${origin}${path}`;
1114
1138
  // the bytes and the type sent: a form's multipart body and boundary, raw bytes, or the body's text
1115
1139
  const sent = formed ? formed.bytes : step.bodyBase64 !== undefined ? Uint8Array.from(Buffer.from(step.bodyBase64, 'base64')) : body === undefined ? undefined : typeof body === 'string' ? body : JSON.stringify(body);
1116
- const instant = clockFile && store.exists(clockFile) ? Date.parse((store.read(clockFile) ?? '').trim()) : Number.NaN;
1117
1140
  const base = { ...(formed ? { 'content-type': formed.contentType } : step.bodyBase64 === undefined ? { 'content-type': defaultType(body) } : {}), ...(journey.headers ?? {}), ...(headers ?? {}) };
1118
- headers = { ...(headers ?? {}), ...sigv4Headers(step.sigv4, captured, step.method, url, base, sent, Number.isNaN(instant) ? 0 : instant) };
1141
+ headers = { ...(headers ?? {}), ...sigv4Headers(step.sigv4, captured, step.method, url, base, sent, Date.parse(worldNow())) };
1119
1142
  }
1120
1143
  }
1121
1144
  catch (e) {
@@ -1128,14 +1151,10 @@ export async function walk(journey, root, collect, opts) {
1128
1151
  continue;
1129
1152
  }
1130
1153
  const abort = new AbortController();
1131
- const timer = setTimeout(() => abort.abort(new Error('deferred request exceeded 10 s')), 10_000);
1132
- const answer = Promise.race([
1133
- send({ ...step, body, headers, ...(formed ? { formed } : {}) }, path, abort.signal),
1134
- new Promise((_, reject) => abort.signal.addEventListener('abort', () => reject(abort.signal.reason), { once: true })),
1135
- ]).finally(() => clearTimeout(timer));
1154
+ const answer = send({ ...step, body, headers, ...(formed ? { formed } : {}) }, path, abort.signal);
1136
1155
  // A rejected body read must not become an unhandled promise while socket steps run.
1137
1156
  void answer.catch(() => undefined);
1138
- pending.set(step.defer, { method: step.method, path, body, headers, answer, abort, timer });
1157
+ pending.set(step.defer, { method: step.method, path, body, headers, answer, abort });
1139
1158
  continue;
1140
1159
  }
1141
1160
  const deferred = step.await ? pending.get(step.await) : undefined;
@@ -1173,43 +1192,70 @@ export async function walk(journey, root, collect, opts) {
1173
1192
  if (deferred) {
1174
1193
  pending.delete(step.await);
1175
1194
  const got = await deferred.answer.then((a) => ({ a }), (error) => ({ error }));
1176
- // a body the vendor broke off, as the collect expects (`breaks`): its words are the answer
1177
1195
  if ('error' in got) {
1178
- if (step.breaks === undefined || !String(got.error).includes(step.breaks))
1179
- collect.push({ step: step.name, beat: 'PENDING', detail: String(got.error) });
1196
+ collect.push({ step: step.name, beat: 'PENDING', detail: String(got.error) });
1180
1197
  continue;
1181
1198
  }
1182
- // over a listener, which ends a broken stream as a short body, the ordinary checks hold the part that arrived
1183
1199
  res = got.a;
1184
1200
  }
1185
- else
1186
- res = await send({ ...step, body, headers, ...(formed ? { formed } : {}) }, path);
1187
- answered = { status: res.status, body: res.body, headers: res.headers };
1201
+ else {
1202
+ try {
1203
+ res = await send({ ...step, body, headers, ...(formed ? { formed } : {}) }, path);
1204
+ }
1205
+ catch (error) {
1206
+ collect.push({ step: step.name, beat: 'CONNECTION', detail: String(error) });
1207
+ continue;
1208
+ }
1209
+ }
1210
+ answered = { status: res.status, body: res.body, headers: res.headers, request: res.request, wireBody: res.wireBody };
1211
+ if (step.breaks !== undefined) {
1212
+ if (!res.bodyError || res.bodyError.cancelled)
1213
+ collect.push({ step: step.name, beat: 'PENDING', detail: 'expected a response body interrupted by the vendor; clean EOF or caller cancellation is not an interruption' });
1214
+ else if (!served && !res.bodyError.message.includes(step.breaks))
1215
+ collect.push({ step: step.name, beat: 'PENDING', detail: res.bodyError.message });
1216
+ }
1217
+ else if (res.bodyError)
1218
+ collect.push({ step: step.name, beat: 'BODY', detail: res.bodyError.message });
1188
1219
  if (opts.verbose)
1189
1220
  console.log(` ${String(res.status).padEnd(4)} ${step.method.padEnd(6)} ${path.slice(0, 70).padEnd(70)} ${step.name}`);
1221
+ // A companion's answer belongs to its own schema, including when the status is unexpected.
1222
+ const stepShape = serving === journey.vendor ? shape : await shapeOf(serving);
1223
+ const companion = companionOf(res.request.url);
1224
+ const databaseDoor = companion ? companion.databaseDoor : factory.managedDatabaseDoor;
1225
+ if (databaseDoor && res.status >= 200 && res.status < 300) {
1226
+ const selectedPath = new URL(stepShape ? stepShape.pathOf(res.request.method, res.request.url, res.request.headers) : res.request.url).pathname;
1227
+ const declared = worldDoorMatcher([{ method: 'POST', path: databaseDoor }])(res.request.method, selectedPath);
1228
+ if (declared) {
1229
+ const connection = (companion?.fetch ?? twin).managedDatabaseConnection;
1230
+ if (!connection || !stepShape)
1231
+ collect.push({ step: step.name, beat: 'ADAPTER', detail: 'The selected native credential transport omitted its connection verification capability' });
1232
+ else {
1233
+ try {
1234
+ const verified = await connection(res.request.method, selectedPath, res.status, res.wireBody);
1235
+ if (!verified)
1236
+ throw new Error('The selected native credential transport supplied no authentication provenance');
1237
+ answered.connection = verified;
1238
+ }
1239
+ catch (error) {
1240
+ collect.push({ step: step.name, beat: 'NATIVE-CONNECTION', detail: error.message });
1241
+ }
1242
+ }
1243
+ }
1244
+ }
1245
+ const problems = responseProblems(stepShape, specFields(serving), {
1246
+ method: res.request.method, path: res.request.url, requestHeaders: res.request.headers,
1247
+ graphqlBody: graphqlBody ? body : undefined, status: res.status, mediaType: res.headers['content-type'], body: res.wireBody, text: res.text,
1248
+ });
1249
+ for (const detail of problems.shape)
1250
+ collect.push({ step: step.name, beat: 'SHAPE', detail });
1251
+ for (const key of problems.leaks)
1252
+ collect.push({ step: step.name, beat: 'LEAK', detail: `answers the twin's own field ${key}` });
1190
1253
  const wanted = step.status;
1191
1254
  const ok = wanted === undefined ? res.status >= 200 && res.status < 300 : res.status === wanted;
1192
1255
  if (!ok) {
1193
1256
  collect.push({ step: step.name, beat: 'STATUS', detail: `${step.method} ${path} answered ${res.status}${wanted ? ` (wanted ${wanted})` : ' (wanted 2xx)'}: ${res.text.slice(0, 160)}` });
1194
1257
  continue;
1195
1258
  }
1196
- // a step a companion served is checked against the companion's spec
1197
- const stepShape = serving === journey.vendor ? shape : await shapeOf(serving);
1198
- const answering = stepShape && res.status >= 200 && res.status < 300 && !stepShape.pinsOlder({ ...(journey.headers ?? {}), ...(headers ?? {}) }) ? stepShape.operationOf(step.method, path, { ...(journey.headers ?? {}), ...(headers ?? {}) }) : undefined;
1199
- if (answering)
1200
- for (const detail of stepShape.failures(answering.op, res.body, res.headers['content-type'], new Headers({ ...(journey.headers ?? {}), ...(headers ?? {}) }).get('accept') ?? undefined, res.status, { method: step.method, responseText: res.text }))
1201
- collect.push({ step: step.name, beat: 'SHAPE', detail });
1202
- // what a pack keeps for itself is `_`-prefixed and never answered, under any name (Protocol 3, State)
1203
- const responseFields = new Set(specFields(serving));
1204
- if (body && typeof body === 'object' && typeof body.query === 'string' && graphqlBody) {
1205
- try {
1206
- visit(parse(body.query), { Field: (field) => { responseFields.add(field.alias?.value ?? field.name.value); } });
1207
- }
1208
- catch { /* malformed documents have no selected response keys */ }
1209
- }
1210
- if (res.status >= 200 && res.status < 300)
1211
- for (const key of leaked(res.body, responseFields))
1212
- collect.push({ step: step.name, beat: 'LEAK', detail: `answers the twin's own field ${key}` });
1213
1259
  // a request that forbids tools (tool_choice none) is answered without a tool call, in any vendor's form
1214
1260
  if (res.status >= 200 && res.status < 300 && step.body && typeof step.body === 'object' && forbidsTools(step.body.tool_choice))
1215
1261
  for (const call of toolCallsIn(res.body))
@@ -1295,30 +1341,50 @@ export async function walk(journey, root, collect, opts) {
1295
1341
  return captured;
1296
1342
  }
1297
1343
  finally {
1298
- for (const request of pending.values()) {
1299
- clearTimeout(request.timer);
1300
- request.abort.abort();
1301
- }
1302
- for (const ws of wire.values())
1303
- ws.close();
1304
- const upgrade = twin.upgrade;
1305
- for (const socket of sockets.values())
1306
- if (!socket.closed)
1307
- upgrade?.close(socket.peer);
1308
- await Promise.allSettled([...pending.values()].map((request) => request.answer));
1309
- setDeliveryObserver(previousObserver);
1310
- setInProcessTwins(previousTwins);
1311
1344
  try {
1312
- if (sqlSession)
1313
- await sqlSession.then((session) => session.end(), () => { });
1345
+ for (const request of pending.values())
1346
+ request.abort.abort();
1347
+ for (const ws of wire.values())
1348
+ ws.close();
1349
+ const upgrade = twin.upgrade;
1350
+ for (const socket of sockets.values())
1351
+ if (!socket.closed)
1352
+ upgrade?.close(socket.peer);
1314
1353
  }
1315
1354
  finally {
1316
- await served?.stop();
1317
- // a World's managed database ends with its walk (vendor-fetch.ts), so the next World declares it at the same port
1318
- await twin.stopDatabase?.();
1355
+ try {
1356
+ await Promise.allSettled([...pending.values()].map((request) => request.answer));
1357
+ await settleEventDeliveries(root);
1358
+ }
1359
+ finally {
1360
+ setDeliveryObserver(previousObserver);
1361
+ setInProcessTwins(previousTwins);
1362
+ setInProcessMailServers(previousMailServers);
1363
+ try {
1364
+ if (sqlSession)
1365
+ await sqlSession.then((session) => session.end(), () => { });
1366
+ }
1367
+ finally {
1368
+ try {
1369
+ await served?.stop();
1370
+ }
1371
+ finally {
1372
+ try {
1373
+ // The selected twin's database ends with its walk.
1374
+ const retired = await Promise.allSettled([twin, ...companions.map(companion => companion.fetch)]
1375
+ .map(fetch => fetch.stopDatabase?.()));
1376
+ const failures = retired.filter(result => result.status === 'rejected').map(result => result.reason);
1377
+ if (failures.length)
1378
+ throw new AggregateError(failures, 'Walk native database retirement failed');
1379
+ }
1380
+ finally {
1381
+ if (clockFile && startedAt !== undefined)
1382
+ store.write(clockFile, startedAt);
1383
+ }
1384
+ }
1385
+ }
1386
+ }
1319
1387
  }
1320
- if (clockFile && startedAt !== undefined)
1321
- store.write(clockFile, startedAt);
1322
1388
  }
1323
1389
  }
1324
1390
  /** Run `fn` with every draw the kernel makes from the platform's random source (the World's seed and keys: a caller in
@@ -10,7 +10,7 @@ export type JourneyStep = {
10
10
  defer?: string;
11
11
  /** Collect a previously deferred request once, with all ordinary response checks. */
12
12
  await?: string;
13
- /** A collect whose answer's body breaks off with these words (a stream the vendor ends in an error), not arriving whole. */
13
+ /** Require an interrupted body; in-process, its error also contains these words. Status and received bytes retain all checks. */
14
14
  breaks?: string;
15
15
  body?: unknown;
16
16
  /** A body of raw bytes (a gzip'd package, an image, an audio file), base64 in the journey and sent decoded: a
@@ -127,7 +127,7 @@ export type JourneyStep = {
127
127
  * whose values the wire encodes (a psdb result's rows, base64 values sliced by per-column lengths) is asserted by
128
128
  * what it holds. Captures read the answer as it came. */
129
129
  view?: string;
130
- /** This step sends a vendor's published example, `<operation>#<title>` (scripts/vendor-examples.ts): its answer
130
+ /** This step sends a vendor's published example, `<operation>#<title>` (tools/vendor-examples.ts): its answer
131
131
  * must hold every key path of the example's answer with the same JSON type. */
132
132
  example?: string;
133
133
  /** Caller-chosen values of the example's named placeholders, compared literally; captures may fill them.
@@ -138,7 +138,7 @@ export type JourneyStep = {
138
138
  exampleDiffers?: Record<string, string>;
139
139
  /** A published body field this step leaves out (a dot path; `a[].b` in each item) → why: a rendered value naming an
140
140
  * object the World does not hold (a Git repository behind an integration). Only a field the request schema does not
141
- * require may be left out (scripts/vendor-examples.ts). */
141
+ * require may be left out (tools/vendor-examples.ts). */
142
142
  unsent?: Record<string, string>;
143
143
  /** answer paths whose value equals something the setup sent without the setup deciding it (a default the vendor applies
144
144
  * that coincides), each with the reason: `vendor-examples.ts --sweep` does not ask them asserted, and the judge reads the reason */
@@ -376,12 +376,13 @@ export declare const isWait: (step: Step) => step is WaitStep;
376
376
  * as written. Its answer is the last statement's `{ command, rows }`, each row its
377
377
  * columns by name in Postgres's text format, which `expect` reads; a refusal fails the step with
378
378
  * Postgres's SQLSTATE, unless the step names that SQLSTATE as its `refused` (the application's own catch of it), when
379
- * its answer is `{ code, message }`. */
379
+ * its answer is `{ code, message }`. `timeOrder` names native timestamp columns whose rows must be nondecreasing. */
380
380
  export type SqlStep = {
381
381
  name: string;
382
382
  sql: string;
383
383
  values?: Array<string | null>;
384
384
  refused?: string;
385
+ timeOrder?: string[];
385
386
  expect?: JourneyStep['expect'];
386
387
  capture?: JourneyStep['capture'];
387
388
  };
@@ -321,14 +321,18 @@ export function fill(template, captured, graphqlDocument = false) {
321
321
  // `{encoded:name}` is a capture percent-encoded (encodeURIComponent), as a URL's path segment or query value or a form
322
322
  // field carries it: a URN a vendor's page prints encoded in its paths (LinkedIn's /rest/posts/urn%3Ali%3Ashare%3A…), a
323
323
  // sign-in's return path holding `&` that a browser posts back
324
- const encoded = basic.replace(/\{encoded:([A-Za-z0-9_.]+)\}/g, (_, k) => {
324
+ const encoded = basic.replace(/\{(encoded|base64|md5):([A-Za-z0-9_.]+)\}/g, (_, transform, k) => {
325
325
  if (captured[k] === undefined)
326
326
  throw new Error(`journey: step refers to {${k}}, which no earlier step captured`);
327
+ if (transform === 'base64')
328
+ return Buffer.from(captured[k], 'utf8').toString('base64');
329
+ if (transform === 'md5')
330
+ return digest('md5', captured[k], 'base64');
327
331
  return encodeURIComponent(captured[k]);
328
332
  });
329
333
  const selections = new Set();
330
334
  // Only request query documents opt into GraphQL grammar (their selection sets, found in the text the captures fill,
331
- // which `{basic:…}` and `{encoded:…}` have already filled); variables remain ordinary captured JSON data.
335
+ // which the credential and byte transforms have already filled); variables remain ordinary captured JSON data.
332
336
  if (graphqlDocument && !Object.hasOwn(captured, /^\{([A-Za-z_][A-Za-z0-9_.]*)\}$/.exec(encoded)?.[1] ?? '')) {
333
337
  try {
334
338
  visit(parse(encoded), { SelectionSet(node) { if (node.loc)
@@ -0,0 +1,23 @@
1
+ /** A received step, with verified native engine provenance outside its unchanged response body. */
2
+ export type ReplayStep = {
3
+ name: string;
4
+ path: string;
5
+ status: number;
6
+ body: unknown;
7
+ sql?: {
8
+ clock: 'wall';
9
+ timeFields: Record<string, 1114 | 1184>;
10
+ };
11
+ connection?: {
12
+ kind: 'postgres';
13
+ field: string;
14
+ authenticated: true;
15
+ };
16
+ };
17
+ /** A journey's declared native timestamp columns are present, valid and nondecreasing down the returned rows. */
18
+ export declare function timestampOrderFailures(body: unknown, timeFields: Record<string, 1114 | 1184>, columns: string[]): string[];
19
+ /** Compare received traces by their selected engine declarations, retaining every undeclared byte. */
20
+ export declare function compareReplays(first: ReadonlyArray<ReplayStep>, second: ReadonlyArray<ReplayStep>): {
21
+ equal: boolean;
22
+ diagnostics: string[];
23
+ };