@volter/twin-standard 1.0.30 → 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 (149) 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 +22 -16
  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/protocol-3.d.ts +1 -0
  72. package/dist/src/protocol-3.js +76 -30
  73. package/dist/src/published.d.ts +6 -0
  74. package/dist/src/published.js +65 -11
  75. package/dist/src/spec-documents.js +2 -1
  76. package/dist/src/spec-ir-client.d.ts +5 -1
  77. package/dist/src/spec-ir-client.js +12 -2
  78. package/dist/src/spec-ir.d.ts +7 -2
  79. package/dist/src/spec-ir.js +42 -10
  80. package/dist/src/verdict.d.ts +14 -0
  81. package/dist/src/verdict.js +30 -0
  82. package/dist/src/walk/behavior-journey.d.ts +47 -0
  83. package/dist/src/walk/behavior-journey.js +1138 -0
  84. package/dist/src/walk/git-client.d.ts +26 -0
  85. package/dist/src/walk/git-client.js +139 -0
  86. package/dist/src/walk/journey-kit.d.ts +445 -0
  87. package/dist/src/walk/journey-kit.js +743 -0
  88. package/dist/src/walk/shape.d.ts +54 -0
  89. package/dist/src/walk/shape.js +210 -0
  90. package/dist/src/walk/sse-example.d.ts +2 -0
  91. package/dist/src/walk/sse-example.js +32 -0
  92. package/dist/src/walk/vendor-fetch.d.ts +20 -0
  93. package/dist/src/walk/vendor-fetch.js +92 -0
  94. package/dist/src/walk/walk-database.d.ts +12 -0
  95. package/dist/src/walk/walk-database.js +96 -0
  96. package/dist/src/walk/xml-view.d.ts +2 -0
  97. package/dist/src/walk/xml-view.js +55 -0
  98. package/package.json +39 -3
  99. package/src/archive-source.ts +141 -0
  100. package/src/check-sources.ts +83 -33
  101. package/src/cli.ts +40 -8
  102. package/src/clients/ai-gateway.ts +4 -0
  103. package/src/clients/anthropic-messages.ts +60 -0
  104. package/src/clients/anthropic.ts +4 -0
  105. package/src/clients/aws.ts +119 -0
  106. package/src/clients/cerebras.ts +4 -0
  107. package/src/clients/clerk.ts +97 -0
  108. package/src/clients/cloudflare.ts +80 -0
  109. package/src/clients/elevenlabs.ts +38 -0
  110. package/src/clients/gemini.ts +89 -0
  111. package/src/clients/github.ts +67 -0
  112. package/src/clients/groq.ts +4 -0
  113. package/src/clients/harness.ts +101 -0
  114. package/src/clients/merge.ts +9 -0
  115. package/src/clients/openai-compatible.ts +59 -0
  116. package/src/clients/openai.ts +66 -0
  117. package/src/clients/openrouter.ts +4 -0
  118. package/src/clients/perplexity.ts +4 -0
  119. package/src/clients/planetscale.ts +98 -0
  120. package/src/clients/posthog.ts +132 -0
  121. package/src/clients/run.ts +18 -0
  122. package/src/clients/sentry.ts +157 -0
  123. package/src/clients/slack.ts +98 -0
  124. package/src/clients/stripe.ts +129 -0
  125. package/src/clients/tinybird.ts +69 -0
  126. package/src/clients/tunnel.ts +79 -0
  127. package/src/clients/upstash.ts +137 -0
  128. package/src/clients/volteridentity.ts +108 -0
  129. package/src/clients/xai.ts +25 -0
  130. package/src/conformance.ts +446 -0
  131. package/src/derive.ts +87 -16
  132. package/src/gate.ts +20 -14
  133. package/src/index.ts +5 -3
  134. package/src/lanes.ts +7 -0
  135. package/src/p3-rules.ts +0 -2
  136. package/src/protocol-3.ts +71 -27
  137. package/src/published.ts +59 -11
  138. package/src/spec-documents.ts +2 -1
  139. package/src/spec-ir-client.ts +15 -4
  140. package/src/spec-ir.ts +43 -5
  141. package/src/verdict.ts +28 -0
  142. package/src/walk/behavior-journey.ts +806 -0
  143. package/src/walk/git-client.ts +138 -0
  144. package/src/walk/journey-kit.ts +858 -0
  145. package/src/walk/shape.ts +208 -0
  146. package/src/walk/sse-example.ts +25 -0
  147. package/src/walk/vendor-fetch.ts +80 -0
  148. package/src/walk/walk-database.ts +97 -0
  149. package/src/walk/xml-view.ts +44 -0
@@ -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>;