@volter/twin-standard 1.0.29 → 1.0.31

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 (152) hide show
  1. package/dist/src/archive-source.d.ts +13 -0
  2. package/dist/src/archive-source.js +176 -0
  3. package/dist/src/check-sources.d.ts +19 -2
  4. package/dist/src/check-sources.js +95 -37
  5. package/dist/src/cli.js +52 -9
  6. package/dist/src/clients/ai-gateway.d.ts +1 -0
  7. package/dist/src/clients/ai-gateway.js +3 -0
  8. package/dist/src/clients/anthropic-messages.d.ts +10 -0
  9. package/dist/src/clients/anthropic-messages.js +52 -0
  10. package/dist/src/clients/anthropic.d.ts +1 -0
  11. package/dist/src/clients/anthropic.js +3 -0
  12. package/dist/src/clients/aws.d.ts +2 -0
  13. package/dist/src/clients/aws.js +137 -0
  14. package/dist/src/clients/cerebras.d.ts +1 -0
  15. package/dist/src/clients/cerebras.js +3 -0
  16. package/dist/src/clients/clerk.d.ts +2 -0
  17. package/dist/src/clients/clerk.js +99 -0
  18. package/dist/src/clients/cloudflare.d.ts +2 -0
  19. package/dist/src/clients/cloudflare.js +86 -0
  20. package/dist/src/clients/elevenlabs.d.ts +2 -0
  21. package/dist/src/clients/elevenlabs.js +39 -0
  22. package/dist/src/clients/gemini.d.ts +2 -0
  23. package/dist/src/clients/gemini.js +92 -0
  24. package/dist/src/clients/github.d.ts +2 -0
  25. package/dist/src/clients/github.js +71 -0
  26. package/dist/src/clients/groq.d.ts +1 -0
  27. package/dist/src/clients/groq.js +3 -0
  28. package/dist/src/clients/harness.d.ts +36 -0
  29. package/dist/src/clients/harness.js +74 -0
  30. package/dist/src/clients/merge.d.ts +1 -0
  31. package/dist/src/clients/merge.js +8 -0
  32. package/dist/src/clients/openai-compatible.d.ts +9 -0
  33. package/dist/src/clients/openai-compatible.js +51 -0
  34. package/dist/src/clients/openai.d.ts +2 -0
  35. package/dist/src/clients/openai.js +65 -0
  36. package/dist/src/clients/openrouter.d.ts +1 -0
  37. package/dist/src/clients/openrouter.js +3 -0
  38. package/dist/src/clients/perplexity.d.ts +1 -0
  39. package/dist/src/clients/perplexity.js +3 -0
  40. package/dist/src/clients/planetscale.d.ts +2 -0
  41. package/dist/src/clients/planetscale.js +109 -0
  42. package/dist/src/clients/posthog.d.ts +2 -0
  43. package/dist/src/clients/posthog.js +145 -0
  44. package/dist/src/clients/run.d.ts +1 -0
  45. package/dist/src/clients/run.js +25 -0
  46. package/dist/src/clients/sentry.d.ts +2 -0
  47. package/dist/src/clients/sentry.js +167 -0
  48. package/dist/src/clients/slack.d.ts +2 -0
  49. package/dist/src/clients/slack.js +107 -0
  50. package/dist/src/clients/stripe.d.ts +2 -0
  51. package/dist/src/clients/stripe.js +161 -0
  52. package/dist/src/clients/tinybird.d.ts +2 -0
  53. package/dist/src/clients/tinybird.js +71 -0
  54. package/dist/src/clients/tunnel.d.ts +2 -0
  55. package/dist/src/clients/tunnel.js +99 -0
  56. package/dist/src/clients/upstash.d.ts +2 -0
  57. package/dist/src/clients/upstash.js +143 -0
  58. package/dist/src/clients/volteridentity.d.ts +2 -0
  59. package/dist/src/clients/volteridentity.js +128 -0
  60. package/dist/src/clients/xai.d.ts +2 -0
  61. package/dist/src/clients/xai.js +23 -0
  62. package/dist/src/conformance.d.ts +33 -0
  63. package/dist/src/conformance.js +520 -0
  64. package/dist/src/derive.js +113 -22
  65. package/dist/src/gate.js +24 -17
  66. package/dist/src/index.d.ts +5 -3
  67. package/dist/src/index.js +5 -3
  68. package/dist/src/lanes.d.ts +2 -0
  69. package/dist/src/lanes.js +7 -0
  70. package/dist/src/p3-rules.js +0 -2
  71. package/dist/src/pack-files.d.ts +2 -0
  72. package/dist/src/pack-files.js +4 -0
  73. package/dist/src/protocol-3.d.ts +1 -0
  74. package/dist/src/protocol-3.js +83 -24
  75. package/dist/src/published.d.ts +6 -0
  76. package/dist/src/published.js +65 -11
  77. package/dist/src/spec-documents.js +2 -1
  78. package/dist/src/spec-ir-client.d.ts +5 -1
  79. package/dist/src/spec-ir-client.js +12 -2
  80. package/dist/src/spec-ir.d.ts +7 -2
  81. package/dist/src/spec-ir.js +42 -10
  82. package/dist/src/verdict.d.ts +14 -0
  83. package/dist/src/verdict.js +30 -0
  84. package/dist/src/walk/behavior-journey.d.ts +47 -0
  85. package/dist/src/walk/behavior-journey.js +1138 -0
  86. package/dist/src/walk/git-client.d.ts +26 -0
  87. package/dist/src/walk/git-client.js +139 -0
  88. package/dist/src/walk/journey-kit.d.ts +445 -0
  89. package/dist/src/walk/journey-kit.js +743 -0
  90. package/dist/src/walk/shape.d.ts +54 -0
  91. package/dist/src/walk/shape.js +210 -0
  92. package/dist/src/walk/sse-example.d.ts +2 -0
  93. package/dist/src/walk/sse-example.js +32 -0
  94. package/dist/src/walk/vendor-fetch.d.ts +20 -0
  95. package/dist/src/walk/vendor-fetch.js +92 -0
  96. package/dist/src/walk/walk-database.d.ts +12 -0
  97. package/dist/src/walk/walk-database.js +96 -0
  98. package/dist/src/walk/xml-view.d.ts +2 -0
  99. package/dist/src/walk/xml-view.js +55 -0
  100. package/package.json +39 -3
  101. package/src/archive-source.ts +141 -0
  102. package/src/check-sources.ts +83 -33
  103. package/src/cli.ts +40 -8
  104. package/src/clients/ai-gateway.ts +4 -0
  105. package/src/clients/anthropic-messages.ts +60 -0
  106. package/src/clients/anthropic.ts +4 -0
  107. package/src/clients/aws.ts +119 -0
  108. package/src/clients/cerebras.ts +4 -0
  109. package/src/clients/clerk.ts +97 -0
  110. package/src/clients/cloudflare.ts +80 -0
  111. package/src/clients/elevenlabs.ts +38 -0
  112. package/src/clients/gemini.ts +89 -0
  113. package/src/clients/github.ts +67 -0
  114. package/src/clients/groq.ts +4 -0
  115. package/src/clients/harness.ts +101 -0
  116. package/src/clients/merge.ts +9 -0
  117. package/src/clients/openai-compatible.ts +59 -0
  118. package/src/clients/openai.ts +66 -0
  119. package/src/clients/openrouter.ts +4 -0
  120. package/src/clients/perplexity.ts +4 -0
  121. package/src/clients/planetscale.ts +98 -0
  122. package/src/clients/posthog.ts +132 -0
  123. package/src/clients/run.ts +18 -0
  124. package/src/clients/sentry.ts +157 -0
  125. package/src/clients/slack.ts +98 -0
  126. package/src/clients/stripe.ts +129 -0
  127. package/src/clients/tinybird.ts +69 -0
  128. package/src/clients/tunnel.ts +79 -0
  129. package/src/clients/upstash.ts +137 -0
  130. package/src/clients/volteridentity.ts +108 -0
  131. package/src/clients/xai.ts +25 -0
  132. package/src/conformance.ts +446 -0
  133. package/src/derive.ts +87 -16
  134. package/src/gate.ts +22 -15
  135. package/src/index.ts +5 -3
  136. package/src/lanes.ts +7 -0
  137. package/src/p3-rules.ts +0 -2
  138. package/src/pack-files.ts +4 -0
  139. package/src/protocol-3.ts +77 -22
  140. package/src/published.ts +59 -11
  141. package/src/spec-documents.ts +2 -1
  142. package/src/spec-ir-client.ts +15 -4
  143. package/src/spec-ir.ts +43 -5
  144. package/src/verdict.ts +28 -0
  145. package/src/walk/behavior-journey.ts +806 -0
  146. package/src/walk/git-client.ts +138 -0
  147. package/src/walk/journey-kit.ts +858 -0
  148. package/src/walk/shape.ts +208 -0
  149. package/src/walk/sse-example.ts +25 -0
  150. package/src/walk/vendor-fetch.ts +80 -0
  151. package/src/walk/walk-database.ts +97 -0
  152. package/src/walk/xml-view.ts +44 -0
@@ -12,7 +12,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
12
12
  // Version 1 checks what the repository's tools can decide today. Criteria the grade section names that no tool
13
13
  // decides yet (a credential stored as issued; hollow capabilities; the invariants row) are listed in PLANNED and
14
14
  // counted in no grade until a version of this standard checks them.
15
+ import { unpublishedUnitFiles } from "./conformance.js";
16
+ import { attributionName, notIndependent } from "./verdict.js";
15
17
  import { spawnSync } from 'node:child_process';
18
+ import { AsyncLocalStorage } from 'node:async_hooks';
16
19
  import { createHash } from 'node:crypto';
17
20
  import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
18
21
  import { tmpdir } from 'node:os';
@@ -21,6 +24,7 @@ import { gunzipSync } from 'node:zlib';
21
24
  import ts from 'typescript';
22
25
  import { fileURLToPath } from 'node:url';
23
26
  import { sharingLanesIn } from "./lanes.js";
27
+ import { isPackArtifact } from "./pack-files.js";
24
28
  /** The lanes over the unit's vendor's state, when the unit is one of them ("Pack layout"), else none: a lane over its
25
29
  * vendor's state takes the vendor's states, shared helpers, manifest and engine from the vendor's src/, and nothing of
26
30
  * another lane. */
@@ -49,31 +53,69 @@ catch {
49
53
  } };
50
54
  /** What spec/ holds beside the vendored documents: provenance, corrections, the grouping, the examples index, recordings. */
51
55
  const SPEC_METADATA = /^(SOURCE\.md|sources\.json|patches\.json|grouping\.json|doc-examples\.json|README\.md|[^/]*\.sha256|recordings\/.*)$/;
52
- /** A tool in spec/ is not a vendored document (it belongs in scripts/): named by the layout criterion, never hashed. */
56
+ /** A tool in spec/ is not a vendored document: never hashed, and outside the layout unless it is the script that wrote
57
+ * the document from the vendor's reference page, which SOURCE.md names (architecture, "Creating a pack", step 2). */
53
58
  const SPEC_TOOL = /\.(py|sh|ts|js|mjs|cjs|rb)$/;
59
+ /** spec/'s tools SOURCE.md names: the writers of a document a vendor publishes only as a page. */
60
+ function namedSpecTools(unit) {
61
+ const path = join(unit.dir, 'spec', 'SOURCE.md');
62
+ const source = existsSync(path) ? readFileSync(path, 'utf8') : '';
63
+ return new Set(filesUnder(join(unit.dir, 'spec')).filter((f) => SPEC_TOOL.test(f) && !f.includes('/') && source.includes(`\`${f}\``)));
64
+ }
54
65
  /** The vendored documents: every file under spec/ that is not its metadata. */
55
66
  function specFiles(unit) {
56
67
  const dir = join(unit.dir, 'spec');
57
68
  return filesUnder(dir).filter((f) => !SPEC_METADATA.test(f) && !SPEC_TOOL.test(f)).map((f) => join(dir, f));
58
69
  }
59
- /** Every file under a directory git does not ignore, relative to it (the checkout's tree, never runtime state). */
70
+ /** The checkout a directory is in: the nearest ancestor holding `.git` (a directory, or a worktree's file). */
71
+ function checkoutOf(dir) {
72
+ for (let d = dir;; d = dirname(d)) {
73
+ if (existsSync(join(d, '.git')))
74
+ return d;
75
+ if (dirname(d) === d)
76
+ return undefined;
77
+ }
78
+ }
79
+ // Grading fans out over a checkout's directories. A caller explicitly owns the immutable listing for one pass;
80
+ // nested gates share it, while concurrent passes have separate snapshots and direct checks always read git afresh.
81
+ const checkoutFiles = new AsyncLocalStorage();
82
+ export function withCheckoutFiles(fn) {
83
+ return checkoutFiles.getStore() ? fn() : checkoutFiles.run(new Map(), fn);
84
+ }
85
+ /** Files relative to a directory: Git's tree in a checkout (one listing per checkout per pass), the disk tree of an
86
+ * unpacked package outside one. A failed Git listing refuses; it never reads as an empty tree. */
60
87
  function filesUnder(dir, base = dir) {
61
88
  if (!existsSync(dir))
62
89
  return [];
63
- // tracked and not-yet-added files alike (T0 runs before the commit); ignored runtime state (.volter) never
64
- const run = spawnSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '.'], { cwd: dir, stdio: ['ignore', 'pipe', 'ignore'] });
65
- const tracked = run.status === 0 ? [...new Set(String(run.stdout).split('\0').filter(Boolean))] : [];
66
- return tracked.map((f) => relative(base, join(dir, f))).filter((f) => existsSync(join(base, f)));
90
+ const top = checkoutOf(dir);
91
+ if (!top)
92
+ return allFilesUnder(dir, base, true);
93
+ const listing = checkoutFiles.getStore();
94
+ let files = listing?.get(top);
95
+ if (!files) {
96
+ // tracked and not-yet-added files alike (T0 runs before the commit); ignored runtime state (.volter) never
97
+ const run = spawnSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '.'], { cwd: top, stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 256 * 1024 * 1024 });
98
+ if (run.status !== 0 || run.error) {
99
+ throw new Error(`git ls-files failed: directory=${JSON.stringify(top)} status=${run.status} signal=${JSON.stringify(run.signal)} error=${JSON.stringify(run.error?.message ?? null)} stderr=${JSON.stringify(String(run.stderr ?? ''))}`);
100
+ }
101
+ files = [...new Set(String(run.stdout).split('\0').filter(Boolean))];
102
+ listing?.set(top, files);
103
+ }
104
+ const under = relative(top, dir);
105
+ const mine = under === '' ? files : files.filter((f) => f.startsWith(`${under}/`)).map((f) => f.slice(under.length + 1));
106
+ return mine.map((f) => relative(base, join(dir, f))).filter((f) => existsSync(join(base, f)));
67
107
  }
68
- /** Every file under a directory, tracked or not (a directory the grader itself wrote). */
69
- function allFilesUnder(dir, base = dir) {
108
+ /** Disk files outside a checkout, or the complete output of the grader's own derivation. */
109
+ function allFilesUnder(dir, base = dir, excludeArtifacts = false) {
70
110
  if (!existsSync(dir))
71
111
  return [];
72
112
  const out = [];
73
113
  for (const name of readdirSync(dir)) {
74
114
  const path = join(dir, name);
115
+ if (excludeArtifacts && isPackArtifact(relative(base, path)))
116
+ continue;
75
117
  if (statSync(path).isDirectory())
76
- out.push(...allFilesUnder(path, base));
118
+ out.push(...allFilesUnder(path, base, excludeArtifacts));
77
119
  else
78
120
  out.push(relative(base, path));
79
121
  }
@@ -97,15 +139,28 @@ const isLane = (unit) => unit.name.includes('/');
97
139
  function srcFileInForm(unit, name) {
98
140
  if (name.endsWith('.d.ts'))
99
141
  return true; // the types of a vendor SDK the pack's tests drive
100
- return (isLane(unit) ? ['manifest.ts', 'index.ts', 'fetch.ts', 'budget.ts'] : ['manifest.ts', 'index.ts', 'cli.ts', 'fetch.ts', 'server.ts', 'budget.ts']).includes(name);
142
+ return (isLane(unit) ? ['manifest.ts', 'index.ts', 'fetch.ts', 'budget.ts'] : ['manifest.ts', 'index.ts', 'cli.ts', 'fetch.ts', 'server.ts', 'stream.ts', 'budget.ts']).includes(name);
101
143
  }
102
144
  /** The fixed files of semantics/ besides the families' handler files and their tests. */
103
- const SEMANTICS_FIXED = new Set(['states.ts', 'index.ts', 'shared.ts', 'doors.ts', 'seed.ts', 'around.ts', 'events.ts', 'clock.ts', 'tenant.ts', 'scenario.ts', 'gap.ts', 'graphql.ts', 'sockets.ts', 'board.ts']);
145
+ const SEMANTICS_FIXED = new Set(['answer-views.ts', 'states.ts', 'index.ts', 'shared.ts', 'doors.ts', 'seed.ts', 'around.ts', 'events.ts', 'clock.ts', 'tenant.ts', 'scenario.ts', 'gap.ts', 'graphql.ts', 'sockets.ts', 'board.ts']);
104
146
  /** journeys/'s closed set. */
105
147
  const JOURNEY_FILES = new Set(['customer-life.outline.md', 'customer-life.outline.verdict.json', 'customer-life.json', 'customer-life.verdict.json', 'decisions.json',
106
148
  'vendor-examples.json', 'vendor-examples.verdict.json', 'demand.json', 'unreachable.json', 'score.json', 'coverage.json',
107
149
  // a generative pack's life names the World's scenario it walks under (architecture, "Writing a life")
108
150
  'scenario.json']);
151
+ /** Only scenario basenames the two walks explicitly declare extend the closed layout. */
152
+ function journeyFiles(unit) {
153
+ const files = new Set(JOURNEY_FILES);
154
+ for (const name of ['customer-life.json', 'vendor-examples.json']) {
155
+ try {
156
+ const journey = JSON.parse(readFileSync(join(unit.dir, 'journeys', name), 'utf8'));
157
+ if (typeof journey.scenario === 'string' && /^[a-z0-9][a-z0-9.-]*\.json$/i.test(journey.scenario))
158
+ files.add(journey.scenario);
159
+ }
160
+ catch { /* malformed or absent journeys get no additional files */ }
161
+ }
162
+ return files;
163
+ }
109
164
  /** Entries at the unit's root the form does not name (a vendor's lanes are units of their own). */
110
165
  function rootOutsideForm(unit) {
111
166
  return entriesOf(unit.dir).filter((name) => {
@@ -144,7 +199,7 @@ const FORBIDDEN_REACH = [
144
199
  [/\bprocess\.env\b|\bBun\.env\b|\bDeno\.env\b/, 'the environment'],
145
200
  [/(^|[^\w])fetch\s*\(|\bXMLHttpRequest\b|\bnode:(net|http|https|tls|dgram)\b/m, 'the network'],
146
201
  [/\bimport\s*\(|\brequire\s*\(/, 'a module loaded at run time'],
147
- [/\bctx\.(now|core|atomically|tree|writeDetailed)\b|\}\s*=\s*ctx\b|\bctx\s*\[/, 'a context member outside the contract, or the context taken apart'],
202
+ [/\bctx\.(now|core|atomically|tree|writeDetailed)\b|\}\s*=\s*ctx\b(?!\s*[.?])|\bctx\s*\[/, 'a context member outside the contract, or the context taken apart'],
148
203
  [/\bSemanticsContext\b|\bSemantics\b|\bsemanticsContext\b/, "the kernel's wide context (a handler is typed Handler, over HandlerContext; a lane reaches its vendor's through ctx.over)"],
149
204
  ];
150
205
  /** A handler's name for an operationId: the id with every character a JavaScript name cannot hold made `_`, and a
@@ -215,7 +270,7 @@ const criteria = [
215
270
  const patches = doc?.patches;
216
271
  if (!Array.isArray(patches))
217
272
  return fail('spec/patches.json is not { patches: [...] }');
218
- const bare = patches.filter((p) => !p.why || !(p.source?.url || p.source?.spec || /https?:\/\//.test(p.why))).length;
273
+ const bare = patches.filter((p) => !p.why || !(p.source?.url || p.source?.spec || p.source?.recording || /https?:\/\//.test(p.why))).length;
219
274
  return bare ? fail(`${bare} of ${patches.length} corrections lack a reason or a source`) : pass(`${patches.length} corrections, each sourced`);
220
275
  },
221
276
  },
@@ -261,7 +316,8 @@ const criteria = [
261
316
  const families = new Set(operationsOf(unit).values());
262
317
  const outside = [
263
318
  ...rootOutsideForm(unit),
264
- ...filesUnder(join(unit.dir, 'spec')).filter((f) => SPEC_TOOL.test(f)).map((f) => `spec/${f}`),
319
+ ...unpublishedUnitFiles(unit.dir),
320
+ ...filesUnder(join(unit.dir, 'spec')).filter((f) => SPEC_TOOL.test(f) && !namedSpecTools(unit).has(f)).map((f) => `spec/${f}`),
265
321
  ...entriesOf(src).filter((name) => statSync(join(src, name)).isDirectory() ? !LAYOUT_DIRS.has(name) : !srcFileInForm(unit, name)).map((n) => `src/${n}`),
266
322
  ...filesUnder(join(src, 'semantics')).filter((f) => {
267
323
  if (f.includes('/'))
@@ -270,7 +326,7 @@ const criteria = [
270
326
  return !(SEMANTICS_FIXED.has(f) || SEMANTICS_FIXED.has(`${stem}.ts`) || (families.has(stem.replace(/-ops$/, '')) && /\.(test\.)?ts$/.test(f)));
271
327
  }).map((f) => `src/semantics/${f}`),
272
328
  // a lane over its vendor's state has no journeys of its own: the vendor's life walks it ("Pack layout")
273
- ...filesUnder(join(unit.dir, 'journeys')).filter((f) => vendorLanes(unit).length > 0 || !JOURNEY_FILES.has(f)).map((f) => `journeys/${f}`),
329
+ ...filesUnder(join(unit.dir, 'journeys')).filter((f) => vendorLanes(unit).length > 0 || !journeyFiles(unit).has(f)).map((f) => `journeys/${f}`),
274
330
  ];
275
331
  // screens/: one file per screen the manifest declares, named by its id
276
332
  if (existsSync(join(src, 'screens'))) {
@@ -353,6 +409,12 @@ const criteria = [
353
409
  found.push(`clock.ts exports ${names.join(', ') || 'nothing'}, not only clock`);
354
410
  continue;
355
411
  }
412
+ if (f === 'answer-views.ts') {
413
+ const names = [...text.matchAll(/^export\s+const\s+([A-Za-z0-9_$]+)/gm)].map((m) => m[1]);
414
+ if (names.join() !== 'answerViews' || /^export\s+(?:async\s+)?function\s|^export\s+(let|var|class)\s|^export\s*\{/m.test(text))
415
+ found.push(`answer-views.ts exports ${names.join(', ') || 'nothing'}, not only answerViews`);
416
+ continue;
417
+ }
356
418
  if (f === 'board.ts') {
357
419
  const names = [...text.matchAll(/^export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/gm)].map((m) => m[1]);
358
420
  if (names.join() !== 'board' || /^export\s+(const|let|var|class)\s|^export\s*\{/m.test(text))
@@ -605,18 +667,15 @@ const criteria = [
605
667
  const verdict = readJson(join(journeys, 'customer-life.outline.verdict.json'));
606
668
  if (!existsSync(outline))
607
669
  return fail('no journeys/customer-life.outline.md');
670
+ const author = /^Author:\s*(.+?)\.?\s*$/m.exec(readFileSync(outline, 'utf8'))?.[1];
671
+ attributionName(author);
608
672
  if (!verdict)
609
673
  return fail('no outline verdict');
610
674
  if (!verdict.plausible)
611
675
  return fail('judged not plausible');
612
- if (/self-judged/i.test(verdict.judge?.session ?? ''))
613
- return fail('self-judged (not a verdict)');
614
- if (!verdict.judge?.agent)
615
- return fail('the verdict does not name its judge (judge.agent)');
616
- if (!verdict.authors?.length)
617
- return fail('the verdict does not name the authors, so the judge is not shown to be another session');
618
- if (verdict.authors.includes(verdict.judge.agent))
619
- return fail('the judge is among the authors');
676
+ const dependent = notIndependent(verdict);
677
+ if (dependent)
678
+ return fail(dependent);
620
679
  return pass(`plausible, judge ${verdict.judge.agent}`);
621
680
  },
622
681
  },
@@ -627,7 +686,7 @@ const criteria = [
627
686
  const r = report(unit);
628
687
  if (!r)
629
688
  return fail(noReport(unit));
630
- return r.life.verdict === 'plausible' ? pass(`${r.life.steps} steps, judge ${r.life.judge}`) : fail(`verdict: ${r.life.verdict}`);
689
+ return r.life.steps > 0 && r.life.verdict === 'plausible' ? pass(`${r.life.steps} steps, judge ${r.life.judge}`) : fail(`verdict: ${r.life.verdict}`);
631
690
  },
632
691
  },
633
692
  {
@@ -1,3 +1,4 @@
1
+ import { type ConformanceResult } from './conformance.js';
1
2
  export type PublishedGate = {
2
3
  spec: string;
3
4
  failures: Array<{
@@ -7,6 +8,11 @@ export type PublishedGate = {
7
8
  };
8
9
  /** The vendor a package twins: its name without its scope and the `twin-` prefix (`@volter/twin-supabase` is supabase). */
9
10
  export declare const vendorOfPackage: (name: string) => string;
11
+ /** The conformance of `spec` as the registry serves it: the published tarball with its own dependencies installed,
12
+ * served and read against the kernel (kernelDir). */
13
+ export declare function conformPublished(spec: string, opts?: {
14
+ registry?: string;
15
+ }): Promise<ConformanceResult[]>;
10
16
  /** The gate over `spec` (`<name>@<version>`) as the registry serves it; throws when it cannot be fetched or unpacked. */
11
17
  export declare function gatePublished(spec: string, opts?: {
12
18
  registry?: string;
@@ -2,14 +2,17 @@
2
2
  // twins come from"): its tarball fetched from the registry, unpacked under its vendor's name (so its lanes are read as
3
3
  // the vendor's), and every unit in it — the pack and each lane — put through the gate.
4
4
  import { spawnSync } from 'node:child_process';
5
- import { existsSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync } from 'node:fs';
5
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, symlinkSync } from 'node:fs';
6
+ import { createRequire } from 'node:module';
6
7
  import { tmpdir } from 'node:os';
7
- import { join } from 'node:path';
8
+ import { dirname, join } from 'node:path';
9
+ import { conformance } from "./conformance.js";
8
10
  import { p3Gate } from "./gate.js";
9
11
  /** The vendor a package twins: its name without its scope and the `twin-` prefix (`@volter/twin-supabase` is supabase). */
10
12
  export const vendorOfPackage = (name) => name.replace(/^@[^/]+\//, '').replace(/^twin-/, '');
11
- /** The gate over `spec` (`<name>@<version>`) as the registry serves it; throws when it cannot be fetched or unpacked. */
12
- export async function gatePublished(spec, opts = {}) {
13
+ /** `spec` (`<name>@<version>`) as the registry serves it, unpacked under its vendor's name in a scratch directory, handed
14
+ * to `use`; throws when it cannot be fetched or unpacked. */
15
+ async function withPublished(spec, opts, use) {
13
16
  const tmp = mkdtempSync(join(tmpdir(), 'twin-standard-'));
14
17
  try {
15
18
  const pack = spawnSync('npm', ['pack', spec, '--pack-destination', tmp, '--silent', ...(opts.registry ? ['--registry', opts.registry] : [])], { encoding: 'utf8' });
@@ -22,15 +25,66 @@ export async function gatePublished(spec, opts = {}) {
22
25
  const name = JSON.parse(readFileSync(join(tmp, 'package', 'package.json'), 'utf8')).name;
23
26
  const dir = join(tmp, vendorOfPackage(name));
24
27
  renameSync(join(tmp, 'package'), dir);
25
- const lanes = readdirSync(dir).filter((l) => existsSync(join(dir, l, 'src', 'manifest.ts')) && !existsSync(join(dir, l, 'package.json'))).map((l) => join(dir, l));
26
- const results = await p3Gate([dir, ...lanes]);
27
- const unit = (u) => u.slice(tmp.length + 1);
28
- return {
29
- spec,
30
- failures: results.filter((r) => r.failures.length).map((r) => ({ unit: unit(r.unit), failures: r.failures })),
31
- };
28
+ return await use(dir, tmp);
32
29
  }
33
30
  finally {
34
31
  rmSync(tmp, { recursive: true, force: true });
35
32
  }
36
33
  }
34
+ /** A platform package a published pack is served with (`@volter/world-core`, `@volter/world-ui`): the one beside this
35
+ * standard (twin-world's, where the catalog clones it), else one the caller installed. Found by its package's entry,
36
+ * walked up to its root (an `exports` that names no `./package.json` is refused by Node). */
37
+ function platformDir(name) {
38
+ for (const from of [import.meta.url, join(process.cwd(), 'package.json')]) {
39
+ let at;
40
+ try {
41
+ at = dirname(createRequire(from).resolve(name));
42
+ }
43
+ catch {
44
+ continue;
45
+ }
46
+ for (; at !== dirname(at); at = dirname(at)) {
47
+ const pkg = join(at, 'package.json');
48
+ if (existsSync(pkg) && JSON.parse(readFileSync(pkg, 'utf8')).name === name)
49
+ return at;
50
+ }
51
+ }
52
+ return undefined;
53
+ }
54
+ /** The conformance of `spec` as the registry serves it: the published tarball with its own dependencies installed,
55
+ * served and read against the kernel (kernelDir). */
56
+ export async function conformPublished(spec, opts = {}) {
57
+ const kernel = platformDir('@volter/world-core');
58
+ if (!kernel)
59
+ throw new Error('no @volter/world-core to serve the pack with: install it beside the standard');
60
+ return withPublished(spec, opts, async (dir) => {
61
+ // what the pack depends on: the platform's packages (`@volter/*`) the same as the kernel, every other (react for its
62
+ // screens, a database client) installed as an application installing the pack gets it
63
+ const deps = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies ?? {};
64
+ const platform = Object.keys(deps).filter((d) => d.startsWith('@volter/'));
65
+ const others = Object.entries(deps).filter(([d]) => !d.startsWith('@volter/')).map(([d, v]) => `${d}@${v}`);
66
+ if (others.length) {
67
+ const install = spawnSync('npm', ['install', '--no-save', '--omit=dev', '--omit=peer', '--ignore-scripts', '--no-audit', '--no-fund', '--no-package-lock', ...(opts.registry ? ['--registry', opts.registry] : []), ...others], { cwd: dirname(dir), encoding: 'utf8' });
68
+ if (install.status !== 0)
69
+ throw new Error(`${spec}: its dependencies do not install (${String(install.stderr).trim().split('\n').at(-1) ?? ''})`);
70
+ }
71
+ mkdirSync(join(dir, 'node_modules', '@volter'), { recursive: true });
72
+ for (const name of ['@volter/world-core', ...platform]) {
73
+ const at = name === '@volter/world-core' ? kernel : platformDir(name);
74
+ if (!at)
75
+ throw new Error(`${spec}: depends on ${name}, which is not beside the standard`);
76
+ rmSync(join(dir, 'node_modules', name), { recursive: true, force: true });
77
+ symlinkSync(at, join(dir, 'node_modules', name));
78
+ }
79
+ return conformance(dir);
80
+ });
81
+ }
82
+ /** The gate over `spec` (`<name>@<version>`) as the registry serves it; throws when it cannot be fetched or unpacked. */
83
+ export async function gatePublished(spec, opts = {}) {
84
+ return withPublished(spec, opts, async (dir, tmp) => {
85
+ const lanes = readdirSync(dir).filter((l) => existsSync(join(dir, l, 'src', 'manifest.ts')) && !existsSync(join(dir, l, 'package.json'))).map((l) => join(dir, l));
86
+ const results = await p3Gate([dir, ...lanes]);
87
+ const unit = (u) => u.slice(tmp.length + 1);
88
+ return { spec, failures: results.filter((r) => r.failures.length).map((r) => ({ unit: unit(r.unit), failures: r.failures })) };
89
+ });
90
+ }
@@ -1,3 +1,4 @@
1
+ import { parseExactJson, exactInteger } from '@volter/world-core/exact-json';
1
2
  // One API published as several OpenAPI documents (Upstash publishes QStash's and Workflow's, both served on
2
3
  // qstash-{region}.upstash.io with one key set, and sharing endpoints): the pack vendors each as published under
3
4
  // `spec/openapi/<name>.{json,yaml}[.gz]`, patches address a document as `/documents/<name>/...`, and the documents are
@@ -17,7 +18,7 @@ export function readSpecDocuments(specDir) {
17
18
  for (const f of readdirSync(dir).filter((f) => /\.(json|ya?ml)(\.gz)?$/.test(f)).sort()) {
18
19
  const bytes = readFileSync(join(dir, f));
19
20
  const text = (f.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('utf8');
20
- documents[f.replace(/\.(json|ya?ml)(\.gz)?$/, '')] = /\.ya?ml(\.gz)?$/.test(f) ? parseYaml(text) : JSON.parse(text);
21
+ documents[f.replace(/\.(json|ya?ml)(\.gz)?$/, '')] = /\.ya?ml(\.gz)?$/.test(f) ? parseExactJson(JSON.stringify(parseYaml(text, { intAsBigInt: true }), (_, v) => typeof v === 'bigint' ? exactInteger(String(v)) : v)) : parseExactJson(text);
21
22
  }
22
23
  if (Object.keys(documents).length < 2)
23
24
  throw new Error(`${dir}: one API in several documents needs at least two; a single document is spec/openapi.{json,yaml}`);
@@ -1,10 +1,13 @@
1
- import type { SpecIR } from './spec-ir.js';
1
+ import type { IrOperation, IrResource, SpecIR } from './spec-ir.js';
2
2
  export type ClientCall = {
3
3
  /** the client method that makes the call, and its line */
4
4
  client: string;
5
5
  line: number;
6
6
  method: string;
7
7
  url: string;
8
+ /** Response declarations supplied by cited reference-page patches, absent from extracted calls. */
9
+ answers?: IrOperation['answers'];
10
+ resource?: string;
8
11
  query?: Record<string, string | null>;
9
12
  body?: {
10
13
  encoding: 'json' | 'form' | 'raw';
@@ -16,5 +19,6 @@ export type ClientOpsSpec = {
16
19
  format: 'client';
17
20
  version: string;
18
21
  calls: ClientCall[];
22
+ resources?: IrResource[];
19
23
  };
20
24
  export declare function fromClientCalls(spec: ClientOpsSpec): SpecIR;
@@ -36,6 +36,14 @@ export function fromClientCalls(spec) {
36
36
  byRoute.set(key, { first: call, path, labels, calls: [call] });
37
37
  }
38
38
  const operations = [...byRoute.values()].map(({ first, path, labels, calls }) => {
39
+ const declared = (key) => {
40
+ const values = calls.map((c) => c[key]).filter((v) => v !== undefined);
41
+ if (values.some((v) => JSON.stringify(v) !== JSON.stringify(values[0])))
42
+ throw new Error(`client response declarations conflict at ${first.method} ${path}: ${key}`);
43
+ return values[0];
44
+ };
45
+ const answers = declared('answers');
46
+ const resource = declared('resource') ?? answers?.resource;
39
47
  const query = new Map();
40
48
  const body = new Map();
41
49
  const encodings = new Set();
@@ -62,7 +70,9 @@ export function fromClientCalls(spec) {
62
70
  body: [...body.values()].sort((a, b) => a.name.localeCompare(b.name)),
63
71
  bodyEncoding,
64
72
  successStatus: 200,
65
- class: 'action',
73
+ class: first.method.toUpperCase() === 'GET' && answers ? (answers.list ? 'list' : 'retrieve') : 'action',
74
+ ...(answers ? { answers } : {}),
75
+ ...(resource ? { resource } : {}),
66
76
  };
67
77
  });
68
78
  // two routes whose first calls share a client method (one method calling twice) keep distinct ids
@@ -73,5 +83,5 @@ export function fromClientCalls(spec) {
73
83
  if (n > 1)
74
84
  o.id = `${o.id}_${n}`;
75
85
  }
76
- return { format: 'client', version: spec.version, resources: [], operations };
86
+ return { format: 'client', version: spec.version, resources: spec.resources ?? [], operations };
77
87
  }
@@ -1,5 +1,7 @@
1
1
  type Json = any;
2
2
  export type OperationClass = 'create' | 'retrieve' | 'list' | 'update' | 'delete' | 'action' | 'computed' | 'non-resource';
3
+ /** Binary media whose resource shape is the response bytes, rather than JSON metadata. */
4
+ export declare const binaryMedia: (media: string) => boolean;
3
5
  export declare const CRUD_CLASSES: ReadonlySet<OperationClass>;
4
6
  /** A parameter or body field: its name, type and whether it is required, and the numeric bounds the spec states
5
7
  * (`minimum`, `maximum`), which a strict body (the manifest's `body.strict`, `ctx.fields`) refuses outside of. */
@@ -7,8 +9,8 @@ export type IrParam = {
7
9
  name: string;
8
10
  type: string;
9
11
  required: boolean;
10
- minimum?: number;
11
- maximum?: number;
12
+ minimum?: number | string;
13
+ maximum?: number | string;
12
14
  };
13
15
  export type IrOperation = {
14
16
  /** The spec's operationId, or the `method_path` slug when the spec has none. */
@@ -36,6 +38,9 @@ export type IrOperation = {
36
38
  /** A form-encoded operation's number and boolean fields, by bracket path (formScalarsOf). */
37
39
  scalars?: Record<string, 'integer' | 'number' | 'boolean'>;
38
40
  successStatus: number;
41
+ /** Binary media types declared by each response status, independent of the twin’s response headers. */
42
+ responseMedia?: Record<string, string[]>;
43
+ defaultResponseMedia?: Record<string, string>;
39
44
  /** the success is documented with no content at all (a 200 with nothing in it) */
40
45
  emptySuccess?: true;
41
46
  /** The success answer may be a server-sent event stream (`text/event-stream`). */
@@ -1,14 +1,11 @@
1
- // THE SPEC IR — one representation of a vendor's published surface, beneath every spec format, that
2
- // the derived-pack generator reads (docs/contributing/architecture.md, "Protocol 3"). A front end
3
- // turns a spec into the IR; the generator only ever sees the IR. `fromOpenAPI` is the first front end, and `fromSmithy`
4
- // reads an AWS Smithy JSON model
5
- // (OpenAPI 3.x; Stripe's vendor extensions `x-resourceId` and `x-expandableFields` are read where
6
- // present). Pure and deterministic: the same document always yields the same IR, byte for byte.
1
+ import { isExactIntegerValue } from '@volter/world-core/exact-json';
2
+ /** Binary media whose resource shape is the response bytes, rather than JSON metadata. */
3
+ export const binaryMedia = (media) => /^(?:application\/(?:octet-stream|pdf|zip|gzip)|(?:image|audio|video)\/[^/]+)$/.test(media.toLowerCase());
7
4
  export const CRUD_CLASSES = new Set(['create', 'retrieve', 'list', 'update', 'delete']);
8
5
  /** A schema's stated numeric bounds, when it states any. */
9
6
  function boundsOf(doc, schema) {
10
7
  const s = deref(doc, schema);
11
- return { ...(typeof s?.minimum === 'number' ? { minimum: s.minimum } : {}), ...(typeof s?.maximum === 'number' ? { maximum: s.maximum } : {}) };
8
+ return { ...((typeof s?.minimum === 'number' || typeof s?.minimum === 'string' && isExactIntegerValue(s.minimum)) ? { minimum: s.minimum } : {}), ...((typeof s?.maximum === 'number' || typeof s?.maximum === 'string' && isExactIntegerValue(s.maximum)) ? { maximum: s.maximum } : {}) };
12
9
  }
13
10
  // every operation an OpenAPI path item may hold (a HEAD is as published as a GET: Vercel's /v8/artifacts/{hash})
14
11
  const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];
@@ -180,6 +177,19 @@ function successOf(doc, op, resources) {
180
177
  const listed = items ? resourcesOfSchema(doc, items, resources) : [];
181
178
  if (listed.length)
182
179
  return { status, answers: answer(listed, true) };
180
+ // a single object under `data` (X's create: a thin data object beside partial errors); a `data` that is an array, named
181
+ // by reference or inline, is a list's envelope, read below
182
+ if (s?.properties?.data && deref(doc, s.properties.data)?.type !== 'array') {
183
+ const payload = s.properties.data;
184
+ const name = refName(payload);
185
+ const found = resourcesOfSchema(doc, payload, resources);
186
+ if (found.length)
187
+ return { status, answers: answer(found, false, 'data') };
188
+ if (name && propsOf(doc, payload).id)
189
+ return { status, answers: answer([name], false, 'data') };
190
+ // A data payload without a modeled identity cannot make the accompanying errors the resource.
191
+ return { status };
192
+ }
183
193
  // An envelope that holds exactly one resource, or one list of them, under a named property, beside
184
194
  // at most a few scalars (`ok`, a cursor). An object with an `id` of its own, or with other objects
185
195
  // in it, is not an envelope: it is something that embeds another (a Pages site, a branch).
@@ -218,7 +228,17 @@ function paramsOf(doc, op, pathItem) {
218
228
  return { pathParams, query };
219
229
  }
220
230
  function bodyOf(doc, op) {
221
- const content = deref(doc, op.requestBody)?.content ?? {};
231
+ const request = deref(doc, op.requestBody);
232
+ const content = request?.content ?? {};
233
+ // A whole binary body is the payload itself, not a JSON object with no fields. Keep the existing blob contract so
234
+ // the kernel leaves its bytes opaque even when a caller labels them application/json.
235
+ const representations = Object.values(content);
236
+ const binary = representations.length > 0 && representations.every((media) => {
237
+ const schema = deref(doc, media?.schema);
238
+ return schema?.type === 'string' && schema.format === 'binary';
239
+ });
240
+ if (binary)
241
+ return { body: [{ name: 'body', type: 'blob', required: request?.required === true }], bodyEncoding: 'none' };
222
242
  const mediaOf = { json: 'application/json', form: 'application/x-www-form-urlencoded', multipart: 'multipart/form-data' };
223
243
  const encoding = content[mediaOf.json] ? 'json' : content[mediaOf.form] ? 'form' : content[mediaOf.multipart] ? 'multipart' : 'none';
224
244
  if (encoding === 'none')
@@ -384,8 +404,8 @@ export function fromOpenAPI(spec) {
384
404
  if (d?.type === 'array')
385
405
  return named(d.items);
386
406
  const props = propsOf(doc, schema);
387
- if (props.data && deref(doc, props.data)?.type === 'array')
388
- return named(deref(doc, props.data).items);
407
+ if (props.data)
408
+ return deref(doc, props.data)?.type === 'array' ? named(deref(doc, props.data).items) : named(props.data);
389
409
  const held = Object.values(props).filter((p) => !envelopeMeta(doc, p)).map((p) => (deref(doc, p)?.type === 'array' ? named(deref(doc, p).items) : named(p))).filter((n) => n.length);
390
410
  return held.length === 1 ? held[0] : [];
391
411
  };
@@ -408,6 +428,17 @@ export function fromOpenAPI(spec) {
408
428
  if (!op)
409
429
  continue;
410
430
  const success = successOf(doc, op, resourceBySchema);
431
+ const responseMedia = Object.fromEntries(Object.entries(op.responses ?? {}).flatMap(([code, response]) => {
432
+ const media = Object.keys(deref(doc, response)?.content ?? {}).filter(binaryMedia).sort();
433
+ return media.length ? [[code, media]] : [];
434
+ }));
435
+ const defaultResponseMedia = Object.fromEntries(Object.entries(op.responses ?? {}).flatMap(([code, response]) => {
436
+ if (!responseMedia[code])
437
+ return [];
438
+ const media = Object.keys(deref(doc, response)?.content ?? {});
439
+ const defaultMedia = media.includes('application/json') ? 'application/json' : media.length === 1 ? media[0] : undefined;
440
+ return defaultMedia ? [[code, defaultMedia]] : [];
441
+ }));
411
442
  const streams = Object.entries(op.responses ?? {}).some(([code, r]) => /^2\d\d$/.test(code) && deref(doc, r)?.content?.['text/event-stream']);
412
443
  const security = (op.security ?? doc.security ?? []);
413
444
  const scopes = [...new Set(security.flatMap((req) => Object.values(req ?? {}).flat()))].sort();
@@ -426,6 +457,7 @@ export function fromOpenAPI(spec) {
426
457
  ...body,
427
458
  ...(scalars ? { scalars } : {}),
428
459
  successStatus: success.status,
460
+ ...(Object.keys(responseMedia).length ? { responseMedia, ...(Object.keys(defaultResponseMedia).length ? { defaultResponseMedia } : {}) } : {}),
429
461
  ...(success.empty ? { emptySuccess: true } : {}),
430
462
  ...(streams ? { streams: true } : {}),
431
463
  ...(scopes.length ? { scopes } : {}),
@@ -0,0 +1,14 @@
1
+ /** Committed attribution names people or author roles, never execution identities. */
2
+ export declare function attributionName(name: string | undefined): string | undefined;
3
+ export type Verdict = {
4
+ judge?: {
5
+ agent?: string;
6
+ name?: string;
7
+ session?: string;
8
+ review?: string;
9
+ };
10
+ authors?: string[];
11
+ };
12
+ /** Why the verdict is no verdict, or undefined when it rules. `author` is the name the judged file itself names as
13
+ * its author (a life's `author`). */
14
+ export declare function notIndependent(verdict: Verdict, author?: string): string | undefined;
@@ -0,0 +1,30 @@
1
+ // A JUDGE'S INDEPENDENCE (the grade's outline-judged and life-judged, the published examples' verdict): a verdict rules
2
+ // only when it names its judge (`judge.agent`) and the authors of what it judges (`authors`), and the judge is none of
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
5
+ // verdict stands, and later changes to what it judged keep it (architecture, "Writing a life", Judge).
6
+ /** Committed attribution names people or author roles, never execution identities. */
7
+ export function attributionName(name) {
8
+ if (name === undefined)
9
+ return undefined;
10
+ if (/\bcx-|\bt_[0-9a-f]{8}|wt\/|\/root(?:\/|$|[: )])|\b(?:pane|session)[ -:]|[0-9a-f]{8}-[0-9a-f-]{27}|\b[0-9a-f]{16,64}\b/i.test(name))
11
+ throw new Error('attribution must use an author name, not an execution identifier');
12
+ return name;
13
+ }
14
+ /** Why the verdict is no verdict, or undefined when it rules. `author` is the name the judged file itself names as
15
+ * its author (a life's `author`). */
16
+ export function notIndependent(verdict, author) {
17
+ const agent = attributionName(verdict.judge?.agent);
18
+ verdict.authors?.forEach(attributionName);
19
+ attributionName(author);
20
+ attributionName(verdict.judge?.session);
21
+ attributionName(verdict.judge?.review);
22
+ const session = attributionName(verdict.judge?.name ?? verdict.judge?.session);
23
+ if (/self-judged/i.test(session ?? '') || (!!author && session === author))
24
+ return 'self-judged (not a verdict)';
25
+ if (!agent || !verdict.authors?.length)
26
+ return 'unattributed (the dispatcher has not stamped judge.agent and authors)';
27
+ if (verdict.authors.includes(agent))
28
+ return `the judge is among the authors (${agent})`;
29
+ return undefined;
30
+ }
@@ -0,0 +1,47 @@
1
+ import { type TwinStream } from '@volter/world-core/runtime';
2
+ import { type Captured, type GitStep, type Journey, type JourneyStep, type LineStep, type RespStep } from './journey-kit.js';
3
+ export declare function usePacks(dir: string): void;
4
+ /** The walk's World: the twin's own code on its in-memory World and blob stores (the backends a serverless
5
+ * entry runs on), so a walk of any length is state changes in memory and never touches the disk; the World
6
+ * clock, pinned to 2026-01-01, is world state in the same store, and the World is sealed. Each root is a fresh key
7
+ * space in it.
8
+ * Restored after ONE run: this module is imported by journey-walk.test.ts, and a store or clock left active
9
+ * would leak into whichever suite the sweep runs next. */
10
+ export declare function inMemoryWorld(label: string): {
11
+ root: () => string;
12
+ restore: () => void;
13
+ };
14
+ export type Failure = {
15
+ step: string;
16
+ beat: string;
17
+ detail: string;
18
+ };
19
+ /** What a caller that scores a walk observes around each step: the step, its resolved path and body,
20
+ * and (after) the answer and the failures the step produced. */
21
+ export type StepHooks = {
22
+ before?: (step: JourneyStep | LineStep | GitStep | RespStep, path: string, body: unknown) => void;
23
+ after?: (step: JourneyStep | LineStep | GitStep | RespStep, path: string, answer: {
24
+ status: number;
25
+ body: unknown;
26
+ headers?: Record<string, string>;
27
+ }, failures: Failure[]) => void;
28
+ };
29
+ /** 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):
30
+ * the walk's requests go to it over HTTP, the vendor's host in their Host header, instead of to the fetch in process;
31
+ * its sockets are WebSockets to that port, and a line connection (`stream`) is a TCP connection to its protocol port. */
32
+ export type ServedTwin = (root: string, options: {
33
+ scenarioPath?: string;
34
+ }) => Promise<{
35
+ fetch: (request: Request) => Promise<Response>;
36
+ stop: () => void;
37
+ socket?: (url: string, headers: Record<string, string>) => WebSocket;
38
+ stream?: TwinStream;
39
+ }>;
40
+ export declare function walk(journey: Journey, root: string, collect: Failure[], opts: {
41
+ beats: boolean;
42
+ verbose?: boolean;
43
+ hooks?: StepHooks;
44
+ captured?: Captured;
45
+ served?: ServedTwin;
46
+ }): Promise<Captured>;
47
+ export declare function withPinnedWorldDraws<T>(fn: () => Promise<T>): Promise<T>;