@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
@@ -0,0 +1,208 @@
1
+ // SHAPE — an answer carries only the fields the spec gives what the operation answers, and a state
2
+ // candidate only the values the spec lists (docs/contributing/architecture.md, "Protocol 3",
3
+ // State and The report), and every field the spec requires. The walk (behavior-journey.ts) runs it on every successful step of a derived
4
+ // pack, so a walk an author iterates with fails where the score would; score-pack reads it from there.
5
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { compileSurface, matchOperation } from '@volter/world-core/runtime';
8
+ import { binaryMedia } from '../spec-ir.ts';
9
+
10
+ export type ShapeOperation = { id: string; method: string; path: string; class: string; resource?: string; successStatus?: number; responseMedia?: Record<string, string[]>; defaultResponseMedia?: Record<string, string>; answers?: { resource: string; list: boolean; key?: string; extends?: string[]; alternatives?: string[] } };
11
+ type Resource = { name: string; fields: Array<{ name: string; required?: boolean; enum?: string[] }> };
12
+ /** Whether a resource is one of the objects `said` names (a union's `object` lists every alternative's). */
13
+ const isObject = (r: Resource, said: unknown): boolean => typeof said === 'string' && (r.fields.find((f) => f.name === 'object')?.enum ?? []).includes(said);
14
+ type Surface = { version: string; basePath?: string; operations: ShapeOperation[]; resources: Resource[] };
15
+
16
+ export type ShapeChecker = {
17
+ /** The spec operation a request matches, if any. An RPC wire answers a method whatever the HTTP method. */
18
+ operationOf: (method: string, path: string, headers?: Record<string, string>) => { op: ShapeOperation; params: Record<string, string> } | undefined;
19
+ /** A request's URL as the unit routes it (a virtual-hosted S3 request's bucket moved from its host into its path). */
20
+ pathOf?: (path: string) => string;
21
+ /** What in an answer the spec does not give the operation's resource (or its view). */
22
+ failures: (op: ShapeOperation, body: unknown, mediaType?: string, accept?: string, status?: number, exchange?: { method: string; responseText: string }) => string[];
23
+ /** Whether a request pins an API version before the spec's, whose answers the spec does not describe (a Stripe
24
+ * mobile SDK's `Stripe-Version: 2024-06-20`), by the version header the manifest declares. */
25
+ pinsOlder: (headers: Record<string, string> | undefined) => boolean;
26
+ };
27
+
28
+ /** The directories under a pack that are lanes: a derived unit with its own generated surface
29
+ * (`../twin-packs-p3/aws/secretsmanager/`, `../twin-packs-p3/planetscale/api/`). */
30
+ export function lanesOf(pkg: string): string[] {
31
+ if (!existsSync(pkg)) return [];
32
+ return readdirSync(pkg, { withFileTypes: true })
33
+ .filter((e) => e.isDirectory() && !['src', 'spec', 'journeys', 'node_modules', 'scripts', 'test-fixtures'].includes(e.name) && existsSync(join(pkg, e.name, 'src', 'generated', 'surface.gen.json')))
34
+ .map((e) => join(pkg, e.name));
35
+ }
36
+
37
+ /** A derived pack's shape checker, or undefined for a pack with no generated surface. A pack's walk reaches its lanes'
38
+ * operations through the pack's router (a psdb life creates its database's passwords through the management API), so
39
+ * a request is matched against the pack's own surface, then each lane's, and judged by the surface it matched. */
40
+ export async function shapeChecker(pkg: string): Promise<ShapeChecker | undefined> {
41
+ const units = (await Promise.all([pkg, ...lanesOf(pkg)].map((dir) => unitChecker(dir)))).filter((c): c is ShapeChecker => !!c);
42
+ if (!units.length) return undefined;
43
+ if (units.length === 1) return units[0];
44
+ const owner = new WeakMap<ShapeOperation, ShapeChecker>();
45
+ return {
46
+ pinsOlder: (headers) => units[0]!.pinsOlder(headers),
47
+ pathOf: (path) => units.find((u) => u.operationOf('GET', path) || u.operationOf('PUT', path))?.pathOf?.(path) ?? path,
48
+ operationOf: (method, path, headers) => {
49
+ for (const u of units) {
50
+ const hit = u.operationOf(method, path, headers);
51
+ if (hit) { owner.set(hit.op, u); return hit; }
52
+ }
53
+ return undefined;
54
+ },
55
+ failures: (op, body, mediaType, accept, status, exchange) => (owner.get(op) ?? units[0]!).failures(op, body, mediaType, accept, status, exchange),
56
+ };
57
+ }
58
+
59
+ /** Accept's most specific media range decides its quality; an explicit q=0 overrides a wildcard. */
60
+ function acceptsMedia(accept: string | undefined, media: string, defaultMedia?: string): boolean {
61
+ if (accept === undefined) return media === defaultMedia?.toLowerCase();
62
+ const [type] = media.split('/');
63
+ const matches = accept.toLowerCase().split(',').flatMap((part) => {
64
+ const [range, ...parameters] = part.trim().split(';').map((value) => value.trim());
65
+ const specificity = range === media ? 2 : range === `${type}/*` ? 1 : range === '*/*' ? 0 : -1;
66
+ if (specificity < 0) return [];
67
+ const quality = parameters.find((value) => value.startsWith('q='));
68
+ const q = quality === undefined ? 1 : Number(quality.slice(2));
69
+ return [{ specificity, q: Number.isFinite(q) && q >= 0 && q <= 1 ? q : 0 }];
70
+ }).sort((a, b) => b.specificity - a.specificity || b.q - a.q);
71
+ return (matches[0]?.q ?? 0) > 0;
72
+ }
73
+
74
+ async function unitChecker(pkg: string): Promise<ShapeChecker | undefined> {
75
+ const surfacePath = join(pkg, 'src', 'generated', 'surface.gen.json');
76
+ if (!existsSync(surfacePath)) return undefined;
77
+ const surface = JSON.parse(readFileSync(surfacePath, 'utf8')) as Surface;
78
+ // a line protocol's surface is its commands' replies (lineShape); a command table's commands ride in a request's body
79
+ // (Upstash's REST envelope), which a check by method and path cannot see: its replies are not shape-checked yet, and
80
+ // its examples assert what each answers
81
+ if (['command-reply', 'command-table'].includes(String((surface as { format?: string }).format))) return undefined;
82
+ const manifestModule = existsSync(join(pkg, 'src', 'manifest.ts')) ? ((await import(join(pkg, 'src', 'manifest.ts'))) as { manifest?: { doors?: ReadonlyArray<{ method: string; path: string }>; worldFields?: Record<string, Record<string, { type: 'array' | 'object' | 'string' | 'number' | 'boolean'; why: string }>>; version?: { header: string }; hosts?: ReadonlyArray<{ host?: string; suffix?: string }>; spanning?: ReadonlyArray<string>; pathFromHost?: string } }) : {};
83
+ // a unit whose host can name part of what its path names (S3's virtual-hosted bucket) is routed as the kernel routes it: a host whose label is part of the path (the manifest's pathFromHost, S3's virtual-hosted
84
+ // addressing) puts it in front of the path
85
+ const hostPath = manifestModule.manifest?.pathFromHost ? new RegExp(manifestModule.manifest.pathFromHost, 'i') : undefined;
86
+ const pathStyle = (u: string): string => {
87
+ const url = new URL(u);
88
+ const label = hostPath?.exec(url.hostname)?.[1];
89
+ return label ? `${url.protocol}//${url.host}/${label}${url.pathname === '/' ? '' : url.pathname}${url.search}` : u;
90
+ };
91
+ // a lane is routed by host ("Other wires: lanes"): one whose manifest names the hosts it serves is judged on those
92
+ // alone, so a page or door of its pack on another host (a dashboard, a World door) is never read as one of its operations
93
+ const hosts = manifestModule.manifest?.hosts;
94
+ const served = (hostname: string): boolean => !hosts || hosts.some((h) => (h.host ? hostname === h.host : !!h.suffix && hostname.endsWith(h.suffix)));
95
+ const versionHeader = manifestModule.manifest?.version?.header.toLowerCase();
96
+ const specDate = /^\d{4}-\d{2}-\d{2}/.exec(String(surface.version))?.[0];
97
+ // the path parameters whose values hold slashes (a git ref, a file path): the surface's own `spanning`, and the
98
+ // manifest's where the spec cannot say
99
+ const spans = [...((surface as { spanning?: string[] }).spanning ?? []), ...(manifestModule.manifest?.spanning ?? [])];
100
+ const routes = compileSurface({ ...surface, ...(spans.length ? { spanning: spans } : {}) });
101
+ const resources = new Map(surface.resources.map((r) => [r.name, r]));
102
+ return {
103
+ pinsOlder: (headers) => {
104
+ if (!versionHeader || !specDate || !headers) return false;
105
+ const pinned = Object.entries(headers).find(([k]) => k.toLowerCase() === versionHeader)?.[1];
106
+ return typeof pinned === 'string' && pinned.slice(0, 10) < specDate;
107
+ },
108
+ pathOf: (path) => (/^https?:\/\//.test(path) ? pathStyle(path) : path),
109
+ operationOf: (method, path, headers) => {
110
+ const url = new URL(/^https?:\/\//.test(path) ? pathStyle(path) : path, 'http://twin.local');
111
+ if (/^https?:\/\//.test(path) && !served(new URL(path).hostname)) return undefined;
112
+ // a World door the manifest declares is that door, never the vendor's operation at a path it would also match
113
+ if (manifestModule.manifest?.doors?.some((door) => door.method.toUpperCase() === method.toUpperCase() && door.path === url.pathname)) return undefined;
114
+ const h = new Headers(headers ?? {});
115
+ const hit = matchOperation(routes, method, url.pathname, url.searchParams, h) ?? [...routes.keys()].map((m) => matchOperation(routes, m, url.pathname, url.searchParams, h)).find(Boolean);
116
+ return hit ? { op: hit.operation as ShapeOperation, params: hit.params } : undefined;
117
+ },
118
+ failures: (op, body, mediaType, accept, status = op.successStatus ?? 200, exchange) => {
119
+ // HEAD's headers describe GET's representation, while HTTP forbids response content.
120
+ if ((exchange?.method ?? op.method).toUpperCase() === 'HEAD') {
121
+ const empty = exchange ? exchange.responseText.length === 0 : body === undefined || body === null || body === '';
122
+ return empty ? [] : ['a HEAD response carries content, which HTTP forbids'];
123
+ }
124
+ const media = mediaType?.split(';')[0]?.trim().toLowerCase();
125
+ const binary = media !== undefined && binaryMedia(media);
126
+ const declared = op.responseMedia?.[String(status)] ?? op.responseMedia?.[`${Math.floor(status / 100)}XX`] ?? op.responseMedia?.default ?? [];
127
+ const defaultMedia = op.defaultResponseMedia?.[String(status)] ?? op.defaultResponseMedia?.[`${Math.floor(status / 100)}XX`] ?? op.defaultResponseMedia?.default;
128
+ if (binary && declared.some((value) => value.toLowerCase() === media) && acceptsMedia(accept, media, defaultMedia)) return [];
129
+ const primary = op.answers ? resources.get(op.answers.resource) : undefined;
130
+ if (!primary) return [];
131
+ if (!body || typeof body !== 'object') return binary ? [`the answer is not the operation's ${primary.name} resource`] : [];
132
+ // an answer that may be one of several resources (a card or a bank account) is judged as the one it says it is
133
+ const alternatives = [primary, ...(op.answers!.alternatives ?? []).map((n) => resources.get(n)).filter((x): x is Resource => !!x)];
134
+ const holder = op.answers!.key ? (body as Record<string, unknown>)[op.answers!.key] : body;
135
+ const items = op.answers!.list ? (Array.isArray(holder) ? holder : (((holder as Record<string, unknown>)?.data as unknown[]) ?? [])) : [holder];
136
+ const out: string[] = [];
137
+ for (const item of items) {
138
+ if (!item || typeof item !== 'object') continue;
139
+ const said = (item as Record<string, unknown>).object;
140
+ // a deleted object is judged as the deleted form of what it says it is
141
+ const gone = (item as Record<string, unknown>).deleted === true;
142
+ const judged = (r: Resource): string[] => {
143
+ const found: string[] = [];
144
+ const known = new Map<string, { enum?: string[] }>([...r.fields.map((f) => [f.name, f] as const), ...(op.answers!.extends ?? []).map((name) => [name, {}] as const)]);
145
+ for (const [k, v] of Object.entries(item as Record<string, unknown>)) {
146
+ const f = known.get(k);
147
+ const local = manifestModule.manifest?.worldFields?.[r.name]?.[k];
148
+ if (local) {
149
+ if (!k.startsWith('twin_') || !local.why?.trim() || f) found.push(`${r.name}.${k}: a World field must have a twin_ name and reason and must not replace a vendor field`);
150
+ const type = Array.isArray(v) ? 'array' : v === null ? 'null' : typeof v;
151
+ if (type !== local.type) found.push(`${r.name}.${k}: World field is ${type}, expected ${local.type}`);
152
+ continue;
153
+ }
154
+ if (!f) found.push(`${r.name} answers \`${k}\`, which the spec does not give it`);
155
+ else if (f.enum && typeof v === 'string' && !f.enum.includes(v)) found.push(`${r.name}.${k} is \`${v}\`, not one of the spec's ${f.enum.join(', ')}`);
156
+ }
157
+ for (const f of r.fields) if (f.required && !(f.name in (item as Record<string, unknown>))) found.push(`${r.name} leaves out \`${f.name}\`, which the spec requires`);
158
+ return found;
159
+ };
160
+ const named = alternatives.find((a) => isObject(a, said) && gone === a.fields.some((f) => f.name === 'deleted'))
161
+ ?? alternatives.find((a) => isObject(a, said));
162
+ // an item that names no `object` is judged as the alternative its own values say it is: one whose every enum field
163
+ // the item carries holds that value (a key's `kty`), the one it fits best where several do
164
+ const consistent = (a: Resource): boolean => a.fields.every((f) => !f.enum || typeof (item as Record<string, unknown>)[f.name] !== 'string' || f.enum.includes((item as Record<string, unknown>)[f.name] as string));
165
+ const r = named ?? (alternatives.length > 1
166
+ ? [...alternatives].filter(consistent).sort((a, b) => judged(a).length - judged(b).length)[0] ?? primary
167
+ : primary);
168
+ out.push(...judged(r));
169
+ }
170
+ return [...new Set(out)];
171
+ },
172
+ };
173
+ }
174
+
175
+ // ── a line protocol's replies ─────────────────────────────────────────────────────────────────
176
+
177
+ /** Where one connection stands for the check: the command whose intermediate reply the next lines answer. */
178
+ export type LineConnection = { pending?: string };
179
+ export type LineShape = {
180
+ /** What is wrong with the replies one line drew (null: the connection's opening), as the spec's
181
+ * command-reply table reads them; moves `conn` past an intermediate reply. */
182
+ check: (conn: LineConnection, line: string | null, codes: number[]) => string[];
183
+ };
184
+
185
+ /** The SHAPE of a line protocol (scripts/derive-pack.ts from an RFC's command-reply table): each reply a
186
+ * code the spec allows the command it answers, or one any command may answer. A line after an
187
+ * intermediate reply (DATA's payload, a SASL response) is answered as that command's `then`. */
188
+ export function lineShape(pkg: string): LineShape | undefined {
189
+ const surfacePath = join(pkg, 'src', 'generated', 'surface.gen.json');
190
+ if (!existsSync(surfacePath)) return undefined;
191
+ type Replies = { intermediate: number[]; success: number[]; error: number[] };
192
+ const surface = JSON.parse(readFileSync(surfacePath, 'utf8')) as { format?: string; anyCommand: number[]; commands: Array<Replies & { id: string; then?: Replies }> };
193
+ if (surface.format !== 'command-reply') return undefined;
194
+ const commands = new Map(surface.commands.map((c) => [c.id, c]));
195
+ return {
196
+ check: (conn, line, codes) => {
197
+ const pending = conn.pending ? commands.get(conn.pending) : undefined;
198
+ const verb = line === null ? 'CONNECTION ESTABLISHMENT' : line.trim().split(/\s/)[0]!.toUpperCase();
199
+ const command = pending ?? commands.get(verb);
200
+ const replies = pending?.then ?? command;
201
+ const allowed = new Set([...surface.anyCommand, ...(replies ? [...replies.intermediate, ...replies.success, ...replies.error] : [])]);
202
+ const out = codes.filter((c) => !allowed.has(c)).map((c) => `${pending ? `the lines after ${pending.id}` : verb} answered ${c}, which the spec does not give it`);
203
+ const last = codes.at(-1);
204
+ if (last !== undefined) conn.pending = command && replies?.intermediate.includes(last) ? command.id : undefined;
205
+ return out;
206
+ },
207
+ };
208
+ }
@@ -0,0 +1,25 @@
1
+ import { parseExactJson } from '@volter/world-core/exact-json';
2
+ /** Match a published SDK-decoded SSE sample to a real HTTP frame; preserve non-SSE answers and mismatches. */
3
+ export function sseExampleAnswer(example: unknown, answer: unknown, matches: (frame: unknown) => boolean): unknown {
4
+ if (typeof answer !== 'string' || !example || typeof example !== 'object') return answer;
5
+ const wrapped = 'data' in example;
6
+ if (!wrapped && !('type' in example) && !('object' in example)) return answer;
7
+ const sample = (wrapped ? example : { data: example }) as { data: unknown; event?: string };
8
+ const frames: Array<{ data: unknown; event?: string }> = [];
9
+ for (const block of answer.replace(/\r\n/g, '\n').split('\n\n')) {
10
+ const lines = block.split('\n');
11
+ const data = lines.filter((s) => s.startsWith('data:')).map((s) => s.slice(5).replace(/^ /, '')).join('\n');
12
+ if (!data || data === '[DONE]') continue;
13
+ const event = lines.find((s) => s.startsWith('event:'))?.slice(6).trim();
14
+ try { frames.push({ data: parseExactJson(data), ...(event ? { event } : {}) }); } catch { /* not a decoded JSON frame */ }
15
+ }
16
+ const relevant = frames.filter((f) => {
17
+ if (sample.event && f.event !== sample.event) return false;
18
+ const want = sample.data as Record<string, unknown> | null;
19
+ const got = f.data as Record<string, unknown> | null;
20
+ return !want || typeof want !== 'object' || !got || typeof got !== 'object'
21
+ || (!want.type || want.type === got.type) && (!want.object || want.object === got.object);
22
+ });
23
+ const candidates = relevant.map((frame) => wrapped ? frame : frame.data);
24
+ return candidates.find(matches) ?? candidates[0] ?? answer;
25
+ }
@@ -0,0 +1,80 @@
1
+ // A vendor's wire, in process: the pack's own fetch factory (`create<Name>Fetch`), found on its
2
+ // index. What the catalog's walkers drive (behavior journeys, the spec census probe, the invariants)
3
+ // since the worker render left with the v1 host.
4
+ import { join } from 'node:path';
5
+ import packFacts from '@volter/world-core/generated/pack-facts.json' with { type: 'json' };
6
+ import { type PgliteHost, startPgliteHost } from './walk-database.ts';
7
+ import { type TwinStream, worldNow } from '@volter/world-core/runtime';
8
+ import type { PackFetchOptions } from '@volter/world-core';
9
+
10
+ type ApplicationStandIn = NonNullable<PackFetchOptions['application']>;
11
+
12
+ /** A pack's fetch over a World's root; a walk whose life names a scenario loads it as the World's (`scenarioPath`), and a
13
+ * life's application answers stand in for the application the vendor asks (`application`, this fetch's alone). */
14
+ export type VendorFetchFactory = (root: string, options?: { scenarioPath?: string; application?: ApplicationStandIn }) => (request: Request) => Response | Promise<Response>;
15
+
16
+ export async function vendorFetch(twinDir: string, vendor: string): Promise<VendorFetchFactory | undefined> {
17
+ let mod: Record<string, unknown>;
18
+ try { mod = (await import(join(twinDir, vendor, 'src', 'index.ts'))) as Record<string, unknown>; } catch { return undefined; }
19
+ // a pack exports create<Name>Fetch (its generated fetch.ts)
20
+ const name = Object.keys(mod).find((k) => /^create\w+?Fetch$/.test(k) && !/TwinFetch$/.test(k) && typeof mod[k] === 'function');
21
+ if (!name) return undefined;
22
+ const factory = mod[name] as (options: { root: string; database?: string; scenarioPath?: string; application?: ApplicationStandIn }) => ((request: Request) => Response | Promise<Response>) & Record<string, unknown>;
23
+ if (!declaresManagedDatabase(vendor)) return (root: string, options = {}) => factory({ root, ...options });
24
+ // A pack whose data plane is the World's managed Postgres is walked over one, as a World binds it: each World (each
25
+ // root) its own fresh database, started on its first request and stopped when the walk's process ends, its clock the
26
+ // walked World's (set before each request: the walk moves the World clock between steps).
27
+ return (root: string, options = {}) => {
28
+ const bare = factory({ root, ...options });
29
+ let bound: Promise<(request: Request) => Response | Promise<Response>> | undefined;
30
+ let host: PgliteHost | undefined;
31
+ const bind = () => (bound ??= startPgliteHost().then((started) => { hosts.push(started); host = started; return factory({ root, database: started.url, ...options }); }));
32
+ const served = (request: Request) => bind().then((fetch) => { host!.clock(worldNow()); return fetch(request); });
33
+ // the World's database itself, for what an application sends it over its own wire (a SQL step: journey-kit.ts)
34
+ const managedDatabaseUrl = () => bind().then(() => { host!.clock(worldNow()); return host!.url; });
35
+ // the walk's end stops its World's database, so the next World walked declares it at the same port
36
+ const stopDatabase = async () => { if (bound === undefined) return; await bound.catch(() => undefined); await host?.stop(); };
37
+ return Object.assign(served, bare, { managedDatabaseUrl, stopDatabase });
38
+ };
39
+ }
40
+
41
+ const hosts: PgliteHost[] = [];
42
+ process.once('exit', () => { for (const host of hosts) { try { process.kill(host.pid, 'SIGTERM'); } catch { /* ended */ } } });
43
+
44
+ /** Whether a pack declares the World's managed database (its descriptor's `managedDatabase`, in the pack facts: this
45
+ * checkout's kernel's, as the walk's other readers of them import them). */
46
+ function declaresManagedDatabase(vendor: string): boolean {
47
+ return (packFacts as { packs: Record<string, { managedDatabase?: unknown }> }).packs[vendor.split('/')[0]!]?.managedDatabase !== undefined;
48
+ }
49
+
50
+ /** Named views of an answer: what a journey step's `view` asserts against. The kit's own: `jwt:<field>`, the payload of the
51
+ * JSON Web Token at that field of the answer, decoded as the application verifying it reads it (a session token's
52
+ * claims). A pack's own (`answerViews` in its index) beside them. */
53
+ export type AnswerViews = Record<string, (body: unknown) => unknown>;
54
+
55
+ const jwtView = (field: string) => (body: unknown): unknown => {
56
+ const token = (body as Record<string, unknown> | null)?.[field];
57
+ if (typeof token !== 'string') return null;
58
+ try { return JSON.parse(Buffer.from(token.split('.')[1] ?? '', 'base64url').toString('utf8')); } catch { return null; }
59
+ };
60
+
61
+ export async function vendorAnswerViews(twinDir: string, vendor: string): Promise<AnswerViews> {
62
+ let own: AnswerViews = {};
63
+ try {
64
+ own = ((await import(join(twinDir, vendor, 'src', 'index.ts'))) as { answerViews?: AnswerViews }).answerViews ?? {};
65
+ } catch { /* none of its own */ }
66
+ return new Proxy(own, { get: (views, name) => (typeof name === 'string' && name.startsWith('jwt:') ? jwtView(name.slice(4)) : views[name as string]) });
67
+ }
68
+
69
+ /** A vendor's byte-stream wire (a line protocol), in process: the pack's `create<Name>TwinStream`, what a
70
+ * hosted World hands a WebSocket's bytes. The walk drives it for a journey's line steps. */
71
+ export type VendorStreamFactory = (root: string, options?: Record<string, unknown>) => TwinStream;
72
+
73
+ export async function vendorStream(twinDir: string, vendor: string): Promise<VendorStreamFactory | undefined> {
74
+ let mod: Record<string, unknown>;
75
+ try { mod = (await import(join(twinDir, vendor, 'src', 'index.ts'))) as Record<string, unknown>; } catch { return undefined; }
76
+ const name = Object.keys(mod).find((k) => /^create\w+TwinStream$/.test(k) && typeof mod[k] === 'function');
77
+ if (!name) return undefined;
78
+ const factory = mod[name] as (options: Record<string, unknown>) => TwinStream;
79
+ return (root: string, options = {}) => factory({ ...options, root });
80
+ }
@@ -0,0 +1,97 @@
1
+ // The walker's managed database: the World's own containerless Postgres (world-runtime's pglite-host.mjs, the PGlite host a World
2
+ // without a container runtime serves its postgres service with), started on a free loopback port in a temp dir and
3
+ // stopped by its own PID, for a pack that declares `managedDatabase` (packages/twin-standard/src/walk/vendor-fetch.ts). Not on the serve path.
4
+ import { spawn, type ChildProcess } from 'node:child_process';
5
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { createServer, connect } from 'node:net';
7
+ import { tmpdir } from 'node:os';
8
+ import { createRequire } from 'node:module';
9
+ import { join } from 'node:path';
10
+
11
+ const HOST_SCRIPT = createRequire(import.meta.url).resolve('@volter/world-runtime/src/pglite-host.mjs');
12
+
13
+ async function freePort(): Promise<number> {
14
+ return new Promise((done, fail) => {
15
+ const server = createServer();
16
+ server.once('error', fail);
17
+ server.listen(0, '127.0.0.1', () => {
18
+ const port = (server.address() as { port: number }).port;
19
+ server.close(() => done(port));
20
+ });
21
+ });
22
+ }
23
+
24
+ function answers(port: number): Promise<boolean> {
25
+ return new Promise((done) => {
26
+ const socket = connect(port, '127.0.0.1');
27
+ socket.once('connect', () => { socket.destroy(); done(true); });
28
+ socket.once('error', () => done(false));
29
+ });
30
+ }
31
+
32
+ /** `clock(at)` sets the World clock the database reads (pglite-host.mjs: the World's time), frozen at the instant `at`. */
33
+ export type PgliteHost = { url: string; port: number; pid: number; clock: (at: string) => void; stop: () => Promise<void> };
34
+
35
+ // The port a walked World declares its postgres service at, as a World's infrastructure file declares one: the same for
36
+ // every World this process walks, so two fresh Worlds hand an application the same database URL (R9), each World's
37
+ // host on it once the one before has stopped (a walk stops its World's host when it ends). A World walked while another
38
+ // still holds it gets a port of its own.
39
+ let declaredPort: number | undefined;
40
+ let declaredHeld = false;
41
+
42
+ async function released(port: number): Promise<boolean> {
43
+ for (const deadline = Date.now() + 5000; Date.now() < deadline; await new Promise((r) => setTimeout(r, 100))) if (!(await answers(port))) return true;
44
+ return false;
45
+ }
46
+
47
+ /** Start a PGlite host under node (PGlite's WASM init does not complete under bun) and wait until it serves. Its clock
48
+ * is a file of the host's own, which the walk sets to the walked World's clock before each request (`clock`); its
49
+ * port is the same for every walked World. Database randomness is not pinned. */
50
+ export async function startPgliteHost(): Promise<PgliteHost> {
51
+ const dir = mkdtempSync(join(tmpdir(), 'walk-database-'));
52
+ const clockFile = join(dir, 'world-clock');
53
+ const declared = declaredPort !== undefined && !declaredHeld && (await released(declaredPort));
54
+ const port = declared ? declaredPort! : await freePort();
55
+ declaredPort ??= port;
56
+ const holdsDeclared = port === declaredPort;
57
+ if (holdsDeclared) declaredHeld = true;
58
+ // The host runs under a small guard that ends it (and removes its data) when this process's end closes the guard's
59
+ // stdin: a test runner or a manifest run that exits without its teardown leaves no PGlite host behind.
60
+ const guard = `
61
+ import { spawn } from 'node:child_process'; import { rmSync } from 'node:fs';
62
+ const [script, port, data, dir] = process.argv.slice(1);
63
+ const host = spawn(process.execPath, [script, '--port', port, '--data', data], { stdio: ['ignore', 'ignore', 'inherit'] });
64
+ const end = () => { try { host.kill('SIGTERM'); } catch {} setTimeout(() => { try { rmSync(dir, { recursive: true, force: true }); } catch {} process.exit(0); }, 500); };
65
+ process.stdin.on('end', end); process.stdin.on('close', end); process.stdin.resume();
66
+ process.on('SIGTERM', end);
67
+ host.on('exit', (code) => { try { rmSync(dir, { recursive: true, force: true }); } catch {} process.exit(code ?? 0); });`;
68
+ const child: ChildProcess = spawn('node', ['--input-type=module', '-e', guard, HOST_SCRIPT, String(port), join(dir, 'pgdata'), dir], { stdio: ['pipe', 'ignore', 'pipe'], env: { ...process.env, TWIN_WORLD_CLOCK_FILE: clockFile } });
69
+ let stderr = '';
70
+ child.stderr?.on('data', (chunk: Buffer) => { stderr += chunk.toString('utf8'); });
71
+ // the host never keeps the walk's process alive: its end closes the guard's stdin, which ends the host (a walk that
72
+ // finished otherwise waited on these pipes forever, and grade-pack on its 600 s timeout)
73
+ child.unref();
74
+ (child.stdin as unknown as { unref?: () => void } | null)?.unref?.();
75
+ (child.stderr as unknown as { unref?: () => void } | null)?.unref?.();
76
+ const deadline = Date.now() + 60_000;
77
+ while (!(await answers(port))) {
78
+ if (child.exitCode !== null) throw new Error(`pglite-host exited ${child.exitCode}: ${stderr}`);
79
+ if (Date.now() > deadline) { child.kill('SIGTERM'); throw new Error(`pglite-host did not listen within 60s: ${stderr}`); }
80
+ await new Promise((r) => setTimeout(r, 200));
81
+ }
82
+ return {
83
+ url: `postgresql://postgres:postgres@127.0.0.1:${port}/postgres`,
84
+ port,
85
+ pid: child.pid!,
86
+ clock: (at) => writeFileSync(clockFile, `${at}\n`),
87
+ stop: async () => {
88
+ if (child.exitCode === null && child.pid !== undefined) {
89
+ const ended = new Promise((r) => child.once('exit', r));
90
+ process.kill(child.pid, 'SIGTERM');
91
+ await Promise.race([ended, new Promise((r) => setTimeout(r, 5000))]);
92
+ }
93
+ rmSync(dir, { recursive: true, force: true });
94
+ if (holdsDeclared) declaredHeld = false;
95
+ },
96
+ };
97
+ }
@@ -0,0 +1,44 @@
1
+ // An XML answer as the members of its root element (S3's `<ListBucketResult><Name>…</Name><Contents>…</Contents>…`,
2
+ // its `<Error><Code>…`): each child element by its name, a repeated one as a list, an element with no children as its
3
+ // text. What a restXml output structure's members are on the wire, so a journey asserts and captures them (`Contents.0.Key`)
4
+ // and SHAPE checks them as it checks a JSON answer's. Attributes and namespaces are not members and are dropped. XML
5
+ // cannot say a lone element is a list: one `<Contents>` reads as an object, two as a list. A root with no children is
6
+ // its one member, by its name.
7
+
8
+ const ENTITIES: Record<string, string> = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
9
+ const decode = (s: string): string => s.replace(/&(#x[0-9a-f]+|#\d+|\w+);/gi, (m, e: string) => (e[0] === '#' ? String.fromCodePoint(e[1] === 'x' || e[1] === 'X' ? parseInt(e.slice(2), 16) : Number(e.slice(1))) : ENTITIES[e] ?? m));
10
+
11
+ type Node = { name: string; children: Node[]; text: string };
12
+
13
+ /** The root element's members, or undefined when the text is not one XML document. */
14
+ export function xmlMembers(text: string): Record<string, unknown> | undefined {
15
+ const src = text.replace(/<\?[\s\S]*?\?>/g, '').replace(/<!--[\s\S]*?-->/g, '').replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, (_, c: string) => c.replace(/&/g, '&amp;').replace(/</g, '&lt;'));
16
+ const stack: Node[] = [{ name: '', children: [], text: '' }];
17
+ const tag = /<(\/?)([\w:.-]+)[^>]*?(\/?)>|([^<]+)/g;
18
+ for (let m = tag.exec(src); m; m = tag.exec(src)) {
19
+ const top = stack.at(-1)!;
20
+ if (m[4] !== undefined) { top.text += m[4]; continue; }
21
+ const name = m[2]!.replace(/^[\w.-]+:/, '');
22
+ if (m[1]) { if (stack.length < 2 || top.name !== name) return undefined; stack.pop(); continue; }
23
+ const node: Node = { name, children: [], text: '' };
24
+ top.children.push(node);
25
+ if (!m[3]) stack.push(node);
26
+ }
27
+ const root = stack.length === 1 && stack[0]!.children.length === 1 ? stack[0]!.children[0]! : undefined;
28
+ if (!root) return undefined;
29
+ // an element with no children is its text; a name that repeats among siblings is a list
30
+ const value = (n: Node): unknown => {
31
+ if (!n.children.length) return decode(n.text.trim());
32
+ const count = new Map<string, number>();
33
+ for (const c of n.children) count.set(c.name, (count.get(c.name) ?? 0) + 1);
34
+ const out: Record<string, unknown> = {};
35
+ for (const c of n.children) {
36
+ if ((count.get(c.name) ?? 0) > 1) ((out[c.name] ??= []) as unknown[]).push(value(c));
37
+ else out[c.name] = value(c);
38
+ }
39
+ return out;
40
+ };
41
+ // a document whose root is a leaf (S3's `<LocationConstraint>auto</LocationConstraint>`) is that one member
42
+ if (!root.children.length) return { [root.name]: value(root) };
43
+ return value(root) as Record<string, unknown>;
44
+ }