@volter/twin-standard 1.0.30 → 1.0.32

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
package/src/published.ts CHANGED
@@ -2,9 +2,11 @@
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, type ConformanceResult } from './conformance.ts';
8
10
  import { p3Gate } from './gate.ts';
9
11
 
10
12
  export type PublishedGate = { spec: string; failures: Array<{ unit: string; failures: string[] }> };
@@ -12,8 +14,9 @@ export type PublishedGate = { spec: string; failures: Array<{ unit: string; fail
12
14
  /** The vendor a package twins: its name without its scope and the `twin-` prefix (`@volter/twin-supabase` is supabase). */
13
15
  export const vendorOfPackage = (name: string): string => name.replace(/^@[^/]+\//, '').replace(/^twin-/, '');
14
16
 
15
- /** The gate over `spec` (`<name>@<version>`) as the registry serves it; throws when it cannot be fetched or unpacked. */
16
- export async function gatePublished(spec: string, opts: { registry?: string } = {}): Promise<PublishedGate> {
17
+ /** `spec` (`<name>@<version>`) as the registry serves it, unpacked under its vendor's name in a scratch directory, handed
18
+ * to `use`; throws when it cannot be fetched or unpacked. */
19
+ async function withPublished<T>(spec: string, opts: { registry?: string }, use: (dir: string, tmp: string) => Promise<T>): Promise<T> {
17
20
  const tmp = mkdtempSync(join(tmpdir(), 'twin-standard-'));
18
21
  try {
19
22
  const pack = spawnSync('npm', ['pack', spec, '--pack-destination', tmp, '--silent', ...(opts.registry ? ['--registry', opts.registry] : [])], { encoding: 'utf8' });
@@ -24,14 +27,59 @@ export async function gatePublished(spec: string, opts: { registry?: string } =
24
27
  const name = (JSON.parse(readFileSync(join(tmp, 'package', 'package.json'), 'utf8')) as { name: string }).name;
25
28
  const dir = join(tmp, vendorOfPackage(name));
26
29
  renameSync(join(tmp, 'package'), dir);
27
- const lanes = readdirSync(dir).filter((l) => existsSync(join(dir, l, 'src', 'manifest.ts')) && !existsSync(join(dir, l, 'package.json'))).map((l) => join(dir, l));
28
- const results = await p3Gate([dir, ...lanes]);
29
- const unit = (u: string): string => u.slice(tmp.length + 1);
30
- return {
31
- spec,
32
- failures: results.filter((r) => r.failures.length).map((r) => ({ unit: unit(r.unit), failures: r.failures })),
33
- };
30
+ return await use(dir, tmp);
34
31
  } finally {
35
32
  rmSync(tmp, { recursive: true, force: true });
36
33
  }
37
34
  }
35
+
36
+ /** A platform package a published pack is served with (`@volter/world-core`, `@volter/world-ui`): the one beside this
37
+ * standard (twin-world's, where the catalog clones it), else one the caller installed. Found by its package's entry,
38
+ * walked up to its root (an `exports` that names no `./package.json` is refused by Node). */
39
+ function platformDir(name: string): string | undefined {
40
+ for (const from of [import.meta.url, join(process.cwd(), 'package.json')]) {
41
+ let at: string;
42
+ try { at = dirname(createRequire(from).resolve(name)); } catch { continue; }
43
+ for (; at !== dirname(at); at = dirname(at)) {
44
+ const pkg = join(at, 'package.json');
45
+ if (existsSync(pkg) && (JSON.parse(readFileSync(pkg, 'utf8')) as { name?: string }).name === name) return at;
46
+ }
47
+ }
48
+ return undefined;
49
+ }
50
+
51
+ /** The conformance of `spec` as the registry serves it: the published tarball with its own dependencies installed,
52
+ * served and read against the kernel (kernelDir). */
53
+ export async function conformPublished(spec: string, opts: { registry?: string } = {}): Promise<ConformanceResult[]> {
54
+ const kernel = platformDir('@volter/world-core');
55
+ if (!kernel) throw new Error('no @volter/world-core to serve the pack with: install it beside the standard');
56
+ return withPublished(spec, opts, async (dir) => {
57
+ // what the pack depends on: the platform's packages (`@volter/*`) the same as the kernel, every other (react for its
58
+ // screens, a database client) installed as an application installing the pack gets it
59
+ const deps = (JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as { dependencies?: Record<string, string> }).dependencies ?? {};
60
+ const platform = Object.keys(deps).filter((d) => d.startsWith('@volter/'));
61
+ const others = Object.entries(deps).filter(([d]) => !d.startsWith('@volter/')).map(([d, v]) => `${d}@${v}`);
62
+ if (others.length) {
63
+ 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' });
64
+ if (install.status !== 0) throw new Error(`${spec}: its dependencies do not install (${String(install.stderr).trim().split('\n').at(-1) ?? ''})`);
65
+ }
66
+ mkdirSync(join(dir, 'node_modules', '@volter'), { recursive: true });
67
+ for (const name of ['@volter/world-core', ...platform]) {
68
+ const at = name === '@volter/world-core' ? kernel : platformDir(name);
69
+ if (!at) throw new Error(`${spec}: depends on ${name}, which is not beside the standard`);
70
+ rmSync(join(dir, 'node_modules', name), { recursive: true, force: true });
71
+ symlinkSync(at, join(dir, 'node_modules', name));
72
+ }
73
+ return conformance(dir);
74
+ });
75
+ }
76
+
77
+ /** The gate over `spec` (`<name>@<version>`) as the registry serves it; throws when it cannot be fetched or unpacked. */
78
+ export async function gatePublished(spec: string, opts: { registry?: string } = {}): Promise<PublishedGate> {
79
+ return withPublished(spec, opts, async (dir, tmp) => {
80
+ const lanes = readdirSync(dir).filter((l) => existsSync(join(dir, l, 'src', 'manifest.ts')) && !existsSync(join(dir, l, 'package.json'))).map((l) => join(dir, l));
81
+ const results = await p3Gate([dir, ...lanes]);
82
+ const unit = (u: string): string => u.slice(tmp.length + 1);
83
+ return { spec, failures: results.filter((r) => r.failures.length).map((r) => ({ unit: unit(r.unit), failures: r.failures })) };
84
+ });
85
+ }
@@ -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
@@ -21,7 +22,7 @@ export function readSpecDocuments(specDir: string): SpecDocuments | undefined {
21
22
  for (const f of readdirSync(dir).filter((f) => /\.(json|ya?ml)(\.gz)?$/.test(f)).sort()) {
22
23
  const bytes = readFileSync(join(dir, f));
23
24
  const text = (f.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('utf8');
24
- documents[f.replace(/\.(json|ya?ml)(\.gz)?$/, '')] = /\.ya?ml(\.gz)?$/.test(f) ? parseYaml(text) : JSON.parse(text);
25
+ 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);
25
26
  }
26
27
  if (Object.keys(documents).length < 2) throw new Error(`${dir}: one API in several documents needs at least two; a single document is spec/openapi.{json,yaml}`);
27
28
  return { documents };
@@ -11,7 +11,7 @@
11
11
  // The calls at one method and path are one operation, their query and body keys together. The client says nothing
12
12
  // of what an answer holds, so an operation answers no resource here: its answers are the vendor's pages' examples.
13
13
 
14
- import type { IrOperation, IrParam, SpecIR } from './spec-ir.ts';
14
+ import type { IrOperation, IrParam, IrResource, SpecIR } from './spec-ir.ts';
15
15
 
16
16
  export type ClientCall = {
17
17
  /** the client method that makes the call, and its line */
@@ -19,10 +19,13 @@ export type ClientCall = {
19
19
  line: number;
20
20
  method: string;
21
21
  url: string;
22
+ /** Response declarations supplied by cited reference-page patches, absent from extracted calls. */
23
+ answers?: IrOperation['answers'];
24
+ resource?: string;
22
25
  query?: Record<string, string | null>;
23
26
  body?: { encoding: 'json' | 'form' | 'raw'; fields?: Record<string, string | null>; passes?: string };
24
27
  };
25
- export type ClientOpsSpec = { format: 'client'; version: string; calls: ClientCall[] };
28
+ export type ClientOpsSpec = { format: 'client'; version: string; calls: ClientCall[]; resources?: IrResource[] };
26
29
 
27
30
  /** A label's name from the expression the client interpolates: `quote(datasource_name, safe='')` is
28
31
  * `datasource_name`, `workspace['id']` is `workspace_id`. */
@@ -51,6 +54,12 @@ export function fromClientCalls(spec: ClientOpsSpec): SpecIR {
51
54
  else byRoute.set(key, { first: call, path, labels, calls: [call] });
52
55
  }
53
56
  const operations: IrOperation[] = [...byRoute.values()].map(({ first, path, labels, calls }) => {
57
+ const declared = <K extends 'answers' | 'resource'>(key: K): ClientCall[K] => {
58
+ const values = calls.map((c) => c[key]).filter((v) => v !== undefined);
59
+ if (values.some((v) => JSON.stringify(v) !== JSON.stringify(values[0]))) throw new Error(`client response declarations conflict at ${first.method} ${path}: ${key}`);
60
+ return values[0];
61
+ };
62
+ const answers = declared('answers'); const resource = declared('resource') ?? answers?.resource;
54
63
  const query = new Map<string, IrParam>();
55
64
  const body = new Map<string, IrParam>();
56
65
  const encodings = new Set<string>();
@@ -72,7 +81,9 @@ export function fromClientCalls(spec: ClientOpsSpec): SpecIR {
72
81
  body: [...body.values()].sort((a, b) => a.name.localeCompare(b.name)),
73
82
  bodyEncoding,
74
83
  successStatus: 200,
75
- class: 'action',
84
+ class: first.method.toUpperCase() === 'GET' && answers ? (answers.list ? 'list' : 'retrieve') : 'action',
85
+ ...(answers ? { answers } : {}),
86
+ ...(resource ? { resource } : {}),
76
87
  };
77
88
  });
78
89
  // two routes whose first calls share a client method (one method calling twice) keep distinct ids
@@ -82,5 +93,5 @@ export function fromClientCalls(spec: ClientOpsSpec): SpecIR {
82
93
  seen.set(o.id, n);
83
94
  if (n > 1) o.id = `${o.id}_${n}`;
84
95
  }
85
- return { format: 'client', version: spec.version, resources: [], operations };
96
+ return { format: 'client', version: spec.version, resources: spec.resources ?? [], operations };
86
97
  }
package/src/spec-ir.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { isExactIntegerValue } from '@volter/world-core/exact-json';
1
2
  // THE SPEC IR — one representation of a vendor's published surface, beneath every spec format, that
2
3
  // the derived-pack generator reads (docs/contributing/architecture.md, "Protocol 3"). A front end
3
4
  // turns a spec into the IR; the generator only ever sees the IR. `fromOpenAPI` is the first front end, and `fromSmithy`
@@ -8,16 +9,19 @@
8
9
  type Json = any;
9
10
 
10
11
  export type OperationClass = 'create' | 'retrieve' | 'list' | 'update' | 'delete' | 'action' | 'computed' | 'non-resource';
12
+ /** Binary media whose resource shape is the response bytes, rather than JSON metadata. */
13
+ export const binaryMedia = (media: string): boolean => /^(?:application\/(?:octet-stream|pdf|zip|gzip)|(?:image|audio|video)\/[^/]+)$/.test(media.toLowerCase());
14
+
11
15
  export const CRUD_CLASSES: ReadonlySet<OperationClass> = new Set(['create', 'retrieve', 'list', 'update', 'delete']);
12
16
 
13
17
  /** A parameter or body field: its name, type and whether it is required, and the numeric bounds the spec states
14
18
  * (`minimum`, `maximum`), which a strict body (the manifest's `body.strict`, `ctx.fields`) refuses outside of. */
15
- export type IrParam = { name: string; type: string; required: boolean; minimum?: number; maximum?: number };
19
+ export type IrParam = { name: string; type: string; required: boolean; minimum?: number | string; maximum?: number | string };
16
20
 
17
21
  /** A schema's stated numeric bounds, when it states any. */
18
- function boundsOf(doc: Json, schema: Json): { minimum?: number; maximum?: number } {
22
+ function boundsOf(doc: Json, schema: Json): { minimum?: number | string; maximum?: number | string } {
19
23
  const s = deref(doc, schema);
20
- return { ...(typeof s?.minimum === 'number' ? { minimum: s.minimum } : {}), ...(typeof s?.maximum === 'number' ? { maximum: s.maximum } : {}) };
24
+ 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 } : {}) };
21
25
  }
22
26
 
23
27
  export type IrOperation = {
@@ -46,6 +50,9 @@ export type IrOperation = {
46
50
  /** A form-encoded operation's number and boolean fields, by bracket path (formScalarsOf). */
47
51
  scalars?: Record<string, 'integer' | 'number' | 'boolean'>;
48
52
  successStatus: number;
53
+ /** Binary media types declared by each response status, independent of the twin’s response headers. */
54
+ responseMedia?: Record<string, string[]>;
55
+ defaultResponseMedia?: Record<string, string>;
49
56
  /** the success is documented with no content at all (a 200 with nothing in it) */
50
57
  emptySuccess?: true;
51
58
  /** The success answer may be a server-sent event stream (`text/event-stream`). */
@@ -245,6 +252,17 @@ function successOf(doc: Json, op: Json, resources: Map<string, string>): { statu
245
252
  const items = s?.type === 'array' ? s.items : s?.properties?.data?.type === 'array' ? s.properties.data.items : undefined;
246
253
  const listed = items ? resourcesOfSchema(doc, items, resources) : [];
247
254
  if (listed.length) return { status, answers: answer(listed, true) };
255
+ // a single object under `data` (X's create: a thin data object beside partial errors); a `data` that is an array, named
256
+ // by reference or inline, is a list's envelope, read below
257
+ if (s?.properties?.data && deref(doc, s.properties.data)?.type !== 'array') {
258
+ const payload = s.properties.data;
259
+ const name = refName(payload);
260
+ const found = resourcesOfSchema(doc, payload, resources);
261
+ if (found.length) return { status, answers: answer(found, false, 'data') };
262
+ if (name && propsOf(doc, payload).id) return { status, answers: answer([name], false, 'data') };
263
+ // A data payload without a modeled identity cannot make the accompanying errors the resource.
264
+ return { status };
265
+ }
248
266
  // An envelope that holds exactly one resource, or one list of them, under a named property, beside
249
267
  // at most a few scalars (`ok`, a cursor). An object with an `id` of its own, or with other objects
250
268
  // in it, is not an envelope: it is something that embeds another (a Pages site, a branch).
@@ -281,7 +299,16 @@ function paramsOf(doc: Json, op: Json, pathItem: Json): { pathParams: string[];
281
299
  }
282
300
 
283
301
  function bodyOf(doc: Json, op: Json): { body: IrParam[]; bodyEncoding: IrOperation['bodyEncoding'] } {
284
- const content = deref(doc, op.requestBody)?.content ?? {};
302
+ const request = deref(doc, op.requestBody);
303
+ const content = request?.content ?? {};
304
+ // A whole binary body is the payload itself, not a JSON object with no fields. Keep the existing blob contract so
305
+ // the kernel leaves its bytes opaque even when a caller labels them application/json.
306
+ const representations = Object.values(content);
307
+ const binary = representations.length > 0 && representations.every((media: Json) => {
308
+ const schema = deref(doc, media?.schema);
309
+ return schema?.type === 'string' && schema.format === 'binary';
310
+ });
311
+ if (binary) return { body: [{ name: 'body', type: 'blob', required: request?.required === true }], bodyEncoding: 'none' };
285
312
  const mediaOf = { json: 'application/json', form: 'application/x-www-form-urlencoded', multipart: 'multipart/form-data' } as const;
286
313
  const encoding = content[mediaOf.json] ? 'json' : content[mediaOf.form] ? 'form' : content[mediaOf.multipart] ? 'multipart' : 'none';
287
314
  if (encoding === 'none') return { body: [], bodyEncoding: 'none' };
@@ -412,7 +439,7 @@ export function fromOpenAPI(spec: Json): SpecIR {
412
439
  const d = deref(doc, schema);
413
440
  if (d?.type === 'array') return named(d.items);
414
441
  const props = propsOf(doc, schema);
415
- if (props.data && deref(doc, props.data)?.type === 'array') return named(deref(doc, props.data).items);
442
+ if (props.data) return deref(doc, props.data)?.type === 'array' ? named(deref(doc, props.data).items) : named(props.data);
416
443
  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);
417
444
  return held.length === 1 ? held[0]! : [];
418
445
  };
@@ -434,6 +461,16 @@ export function fromOpenAPI(spec: Json): SpecIR {
434
461
  const op = (item as Json)[method];
435
462
  if (!op) continue;
436
463
  const success = successOf(doc, op, resourceBySchema);
464
+ const responseMedia = Object.fromEntries(Object.entries(op.responses ?? {}).flatMap(([code, response]) => {
465
+ const media = Object.keys(deref(doc, response)?.content ?? {}).filter(binaryMedia).sort();
466
+ return media.length ? [[code, media]] : [];
467
+ }));
468
+ const defaultResponseMedia = Object.fromEntries(Object.entries(op.responses ?? {}).flatMap(([code, response]) => {
469
+ if (!responseMedia[code]) return [];
470
+ const media = Object.keys(deref(doc, response)?.content ?? {});
471
+ const defaultMedia = media.includes('application/json') ? 'application/json' : media.length === 1 ? media[0] : undefined;
472
+ return defaultMedia ? [[code, defaultMedia]] : [];
473
+ }));
437
474
  const streams = Object.entries(op.responses ?? {}).some(([code, r]) => /^2\d\d$/.test(code) && deref(doc, r)?.content?.['text/event-stream']);
438
475
  const security = (op.security ?? doc.security ?? []) as Json[];
439
476
  const scopes = [...new Set(security.flatMap((req: Json) => Object.values(req ?? {}).flat() as string[]))].sort();
@@ -452,6 +489,7 @@ export function fromOpenAPI(spec: Json): SpecIR {
452
489
  ...body,
453
490
  ...(scalars ? { scalars } : {}),
454
491
  successStatus: success.status,
492
+ ...(Object.keys(responseMedia).length ? { responseMedia, ...(Object.keys(defaultResponseMedia).length ? { defaultResponseMedia } : {}) } : {}),
455
493
  ...(success.empty ? { emptySuccess: true as const } : {}),
456
494
  ...(streams ? { streams: true } : {}),
457
495
  ...(scopes.length ? { scopes } : {}),
package/src/verdict.ts ADDED
@@ -0,0 +1,28 @@
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
+
7
+ /** Committed attribution names people or author roles, never execution identities. */
8
+ export function attributionName(name: string | undefined): string | undefined {
9
+ if (name === undefined) 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)) throw new Error('attribution must use an author name, not an execution identifier');
11
+ return name;
12
+ }
13
+ export type Verdict = { judge?: { agent?: string; name?: string; session?: string; review?: string }; authors?: string[] };
14
+
15
+ /** Why the verdict is no verdict, or undefined when it rules. `author` is the name the judged file itself names as
16
+ * its author (a life's `author`). */
17
+ export function notIndependent(verdict: Verdict, author?: string): string | undefined {
18
+ const agent = attributionName(verdict.judge?.agent);
19
+ verdict.authors?.forEach(attributionName);
20
+ attributionName(author);
21
+ attributionName(verdict.judge?.session);
22
+ attributionName(verdict.judge?.review);
23
+ const session = attributionName(verdict.judge?.name ?? verdict.judge?.session);
24
+ if (/self-judged/i.test(session ?? '') || (!!author && session === author)) return 'self-judged (not a verdict)';
25
+ if (!agent || !verdict.authors?.length) return 'unattributed (the dispatcher has not stamped judge.agent and authors)';
26
+ if (verdict.authors.includes(agent)) return `the judge is among the authors (${agent})`;
27
+ return undefined;
28
+ }