@volter/twin-standard 1.0.158 → 1.0.159

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 (69) hide show
  1. package/README.md +3 -2
  2. package/client-sdks/package.json +1 -0
  3. package/client-sdks/pnpm-lock.yaml +210 -0
  4. package/dist/client-sdks/package.json +1 -0
  5. package/dist/client-sdks/pnpm-lock.yaml +210 -0
  6. package/dist/src/assessment.d.ts +13 -3
  7. package/dist/src/assessment.js +10 -6
  8. package/dist/src/browser-assessment.js +16 -13
  9. package/dist/src/clients/harness.d.ts +25 -4
  10. package/dist/src/clients/harness.js +184 -22
  11. package/dist/src/clients/jira.js +11 -2
  12. package/dist/src/clients/run.js +19 -14
  13. package/dist/src/conformance.d.ts +2 -0
  14. package/dist/src/conformance.js +392 -184
  15. package/dist/src/consumer.d.ts +22 -1
  16. package/dist/src/consumer.js +91 -26
  17. package/dist/src/credential-issuer-import.d.ts +3 -0
  18. package/dist/src/credential-issuer-import.js +42 -0
  19. package/dist/src/derive.js +3 -0
  20. package/dist/src/gate.js +4 -0
  21. package/dist/src/lanes.d.ts +2 -0
  22. package/dist/src/lanes.js +7 -0
  23. package/dist/src/protocol-3.js +8 -3
  24. package/dist/src/spec-documents.d.ts +4 -0
  25. package/dist/src/spec-documents.js +110 -3
  26. package/dist/src/tools/life-coverage-walk.js +38 -26
  27. package/dist/src/tools/score-pack.js +6 -5
  28. package/dist/src/tools/vendor-examples.js +91 -80
  29. package/dist/src/walk/behavior-journey.d.ts +22 -14
  30. package/dist/src/walk/behavior-journey.js +182 -133
  31. package/dist/src/walk/journey-kit.d.ts +3 -2
  32. package/dist/src/walk/journey-kit.js +6 -2
  33. package/dist/src/walk/replay.d.ts +23 -0
  34. package/dist/src/walk/replay.js +118 -0
  35. package/dist/src/walk/serve-pack.d.ts +2 -2
  36. package/dist/src/walk/serve-pack.js +38 -2
  37. package/dist/src/walk/shape.d.ts +25 -4
  38. package/dist/src/walk/shape.js +166 -56
  39. package/dist/src/walk/vendor-fetch.d.ts +7 -2
  40. package/dist/src/walk/vendor-fetch.js +13 -2
  41. package/dist/src/walk/walk-database.d.ts +9 -1
  42. package/dist/src/walk/walk-database.js +35 -13
  43. package/dist/src/walk/xml-view.d.ts +3 -2
  44. package/dist/src/walk/xml-view.js +5 -4
  45. package/package.json +3 -3
  46. package/src/assessment.ts +12 -8
  47. package/src/browser-assessment.ts +12 -5
  48. package/src/clients/harness.ts +142 -19
  49. package/src/clients/jira.ts +4 -2
  50. package/src/clients/run.ts +17 -15
  51. package/src/conformance.ts +313 -128
  52. package/src/consumer.ts +83 -21
  53. package/src/credential-issuer-import.ts +38 -0
  54. package/src/derive.ts +3 -1
  55. package/src/gate.ts +3 -0
  56. package/src/lanes.ts +7 -0
  57. package/src/protocol-3.ts +7 -3
  58. package/src/spec-documents.ts +92 -3
  59. package/src/tools/life-coverage-walk.ts +32 -22
  60. package/src/tools/score-pack.ts +7 -6
  61. package/src/tools/vendor-examples.ts +88 -75
  62. package/src/walk/behavior-journey.ts +145 -109
  63. package/src/walk/journey-kit.ts +7 -5
  64. package/src/walk/replay.ts +103 -0
  65. package/src/walk/serve-pack.ts +25 -5
  66. package/src/walk/shape.ts +156 -54
  67. package/src/walk/vendor-fetch.ts +16 -4
  68. package/src/walk/walk-database.ts +41 -15
  69. package/src/walk/xml-view.ts +5 -4
package/src/consumer.ts CHANGED
@@ -19,13 +19,28 @@ export type ConsumerRecipe = {
19
19
  };
20
20
  export type ConsumerOptions = { appDir: string; cli: string; artifactIntegrity: string };
21
21
  type ConsumerInstallation = { cli: string; artifact?: string };
22
+ export type ConsumerResult = Awaited<ReturnType<typeof runConsumerWorkflow>>;
23
+
24
+ function recipeDependencies(recipe: ConsumerRecipe): Record<string, string> {
25
+ const app = JSON.parse(recipe.files['package.json']!);
26
+ const dependencies = { ...app.dependencies, ...app.devDependencies, ...app.optionalDependencies };
27
+ for (const [name, version] of Object.entries(dependencies)) {
28
+ if (typeof version !== 'string' || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) throw new Error(`${name}: customer SDK dependencies require exact registry versions`);
29
+ }
30
+ return dependencies;
31
+ }
22
32
 
23
33
  function packageIdentity(entry: string, name: string): { directory: string; version: string } {
24
34
  let directory = dirname(realpathSync(entry));
25
35
  while (true) {
26
- const file = join(directory, 'package.json');
27
- if (existsSync(file)) { const pkg = JSON.parse(readFileSync(file, 'utf8')); if (pkg.name === name) return { directory, version: pkg.version }; }
28
36
  const parent = dirname(directory);
37
+ // The installed node_modules slot owns the entry; nested distribution manifests do not identify the package.
38
+ const boundary = basename(parent) === 'node_modules' || (basename(parent).startsWith('@') && basename(dirname(parent)) === 'node_modules');
39
+ if (boundary) {
40
+ const pkg = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));
41
+ if (pkg.name !== name) throw new Error(`${name}: installed package boundary belongs to ${String(pkg.name)}`);
42
+ return { directory, version: pkg.version };
43
+ }
29
44
  if (parent === directory) throw new Error(`${name} has no installed package identity`);
30
45
  directory = parent;
31
46
  }
@@ -39,6 +54,21 @@ function consumerTools(cli: string) {
39
54
  };
40
55
  }
41
56
 
57
+ function consumerEntries(directory: string, names: string[]): Map<string, string> {
58
+ if (!names.length) return new Map();
59
+ const result = spawnSync('node', ['--input-type=module', '--eval', 'process.stdout.write(JSON.stringify(process.argv.slice(1).map(name => import.meta.resolve(name))))', ...names], { cwd: directory, encoding: 'utf8' });
60
+ if (result.error || result.status !== 0) throw new Error(`Consumer ESM dependency resolution failed: ${result.error?.message ?? result.stderr.trim()}`);
61
+ const entries = JSON.parse(result.stdout) as string[];
62
+ return new Map(names.map((name, index) => [name, fileURLToPath(entries[index]!)]));
63
+ }
64
+
65
+ function consumerPeerVersions(metadata: { dependencies?: Record<string, string>; optionalDependencies?: Record<string, string>; peerDependencies?: Record<string, string>; peerDependenciesMeta?: Record<string, { optional?: boolean }> }, cli: string, sdkDependencies: Record<string, string>): Record<string, string> {
66
+ const names = Object.keys(metadata.peerDependencies ?? {})
67
+ .filter((name) => name !== '@volter/world-core' && !metadata.peerDependenciesMeta?.[name]?.optional && !metadata.dependencies?.[name] && !metadata.optionalDependencies?.[name] && !sdkDependencies[name]);
68
+ const entries = consumerEntries(dirname(realpathSync(cli)), names);
69
+ return Object.fromEntries(names.map((name) => [name, packageIdentity(entries.get(name)!, name).version]));
70
+ }
71
+
42
72
  /** Bind behavior inputs on both source and packed layouts; compiled outputs and score caches are not inputs. */
43
73
  export function consumerSourceDigest(pack: string): string {
44
74
  const entries: Array<[string, string]> = [];
@@ -83,9 +113,7 @@ export function prepareConsumerApp(packDir: string, appDir: string, installation
83
113
  app.packageManager = 'pnpm@12.15.0';
84
114
  app.devEngines = { packageManager: { name: 'pnpm', version: '12.15.0', onFail: 'error' } };
85
115
  if (app.scripts || app.workspaces) throw new Error('Consumer fixture supplies direct SDK commands, not install scripts or workspace links');
86
- for (const [name, version] of Object.entries({ ...app.dependencies, ...app.devDependencies, ...app.optionalDependencies })) {
87
- if (typeof version !== 'string' || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) throw new Error(`${name}: customer SDK dependencies require exact registry versions`);
88
- }
116
+ const sdkDependencies = recipeDependencies(recipe);
89
117
  const files = Object.entries(recipe.files).map(([path, content]) => {
90
118
  if (isAbsolute(path) || path.includes('\\') || path.split('/').some((part) => !part || part === '.' || part === '..' || part === '.git') || path.split('/').includes('node_modules') || ['.env', '.npmrc', '.volter/world.json'].includes(path)) throw new Error(`Invalid consumer file: ${path}`);
91
119
  if (typeof content !== 'string') throw new Error('Consumer files must contain text');
@@ -96,7 +124,7 @@ export function prepareConsumerApp(packDir: string, appDir: string, installation
96
124
  const tools = consumerTools(installation.cli);
97
125
  if (installation.artifact && (!isAbsolute(installation.artifact) || !statSync(installation.artifact).isFile())) throw new Error('Consumer candidate must be an absolute prepared archive path');
98
126
  if (Object.hasOwn(app.dependencies ?? {}, metadata.name)) throw new Error('Consumer SDK dependencies must not substitute the candidate package');
99
- app.devDependencies = { ...app.devDependencies, '@volter/world-core': tools['@volter/world-core'], [metadata.name]: installation.artifact ? `file:${installation.artifact}` : metadata.version };
127
+ app.devDependencies = { ...app.devDependencies, ...consumerPeerVersions(metadata, installation.cli, sdkDependencies), '@volter/world-core': tools['@volter/world-core'], [metadata.name]: installation.artifact ? `file:${installation.artifact}` : metadata.version };
100
128
  }
101
129
  files.find(([path]) => path === 'package.json')![1] = JSON.stringify(app, null, 2) + '\n';
102
130
  files.push(['pnpm-workspace.yaml', 'packages:\n - .\npackageImportMethod: hardlink\nnodeLinker: hoisted\nautoInstallPeers: false\nstrictPeerDependencies: false\n']);
@@ -118,40 +146,56 @@ export function prepareConsumerApp(packDir: string, appDir: string, installation
118
146
  return { recipeSha256: createHash('sha256').update(JSON.stringify(recipe)).digest('hex'), dependencies: app.dependencies ?? {} };
119
147
  }
120
148
 
149
+ /** Run the unchanged customer command, with the environment supplied by its World. */
150
+ export function runConsumerCommand(argv: string[], appDir: string, env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<{ exitCode: number | null; stdout: string; stderr: string }> {
151
+ return new Promise((done) => {
152
+ const child = spawn(argv[0]!, argv.slice(1), { cwd: appDir, env, signal, stdio: ['ignore', 'pipe', 'pipe'] });
153
+ let stdout = '', stderr = '';
154
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
155
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
156
+ child.on('error', (error) => { stderr += `\n${error.message}`; });
157
+ child.on('close', (exitCode) => done({ exitCode, stdout, stderr }));
158
+ });
159
+ }
160
+
121
161
  export async function runConsumerWorkflow(packDir: string, options: ConsumerOptions) {
122
162
  if (!isAbsolute(options.appDir) || !isAbsolute(options.cli) || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(options.artifactIntegrity)) throw new Error('Consumer requires absolute prepared paths and candidate SHA-512 integrity');
123
163
  if (existsSync(join(options.appDir, '.volter/world.json'))) throw new Error('Consumer app already has a World; prepare a new app rather than reusing prior state');
124
164
  const cliEntry = realpathSync(options.cli);
125
165
  const tools = consumerTools(cliEntry);
126
166
  const metadata = JSON.parse(readFileSync(join(packDir, 'package.json'), 'utf8'));
167
+ const facts = JSON.parse(readFileSync(join(packDir, 'generated/pack-facts.json'), 'utf8'));
168
+ const vendor = Object.keys(facts.packs ?? {})[0];
169
+ if (!vendor || Object.keys(facts.packs).length !== 1) throw new Error('Consumer workflow requires one selected artifact identity');
170
+ const recipe = consumerRecipe(vendor, packDir), sdkDependencies = recipeDependencies(recipe);
127
171
  const appRoot = realpathSync(options.appDir), appRequire = createRequire(join(appRoot, 'package.json'));
128
172
  const candidate = packageIdentity(appRequire.resolve(metadata.name), metadata.name);
129
173
  const kernel = packageIdentity(appRequire.resolve('@volter/world-core/runtime'), '@volter/world-core');
130
- for (const [name, identity] of [[metadata.name, candidate], ['@volter/world-core', kernel]] as const) {
174
+ const peerVersions = consumerPeerVersions(metadata, cliEntry, sdkDependencies);
175
+ const entries = consumerEntries(appRoot, [...Object.keys(peerVersions), ...Object.keys(sdkDependencies)]);
176
+ const peers = Object.keys(peerVersions).map((name) => [name, packageIdentity(entries.get(name)!, name)] as const);
177
+ for (const [name, identity] of [[metadata.name, candidate], ['@volter/world-core', kernel], ...peers] as const) {
131
178
  const location = relative(join(appRoot, 'node_modules'), identity.directory);
132
179
  if (location.startsWith('..') || isAbsolute(location)) throw new Error(`${name}: consumer resolves an evaluator or ancestor dependency; install it in the app`);
133
180
  }
134
181
  const appLock = dependencyLock(appRoot), appLockBytes = appLock.bytes;
135
182
  if (candidate.version !== metadata.version || appLock.entry(metadata.name)?.integrity !== options.artifactIntegrity) throw new Error('Consumer app candidate does not match the exact assessed artifact');
136
183
  if (kernel.version !== tools['@volter/world-core'] || appLock.entry('@volter/world-core')?.version !== kernel.version) throw new Error('Consumer app kernel does not match the CLI kernel pin');
137
- const application = { candidate: { package: metadata.name, version: candidate.version, integrity: options.artifactIntegrity }, kernel: { package: '@volter/world-core', version: kernel.version }, dependencyLockSha256: createHash('sha256').update(appLockBytes).digest('hex'), appLocal: true };
138
- const facts = JSON.parse(readFileSync(join(packDir, 'generated/pack-facts.json'), 'utf8'));
139
- const vendor = Object.keys(facts.packs ?? {})[0];
140
- if (!vendor || Object.keys(facts.packs).length !== 1) throw new Error('Consumer workflow requires one selected artifact identity');
141
- const recipe = consumerRecipe(vendor, packDir);
184
+ for (const [name, identity] of peers) if (identity.version !== peerVersions[name] || appLock.entry(name)?.version !== identity.version) throw new Error(`${name}: consumer peer does not match the installed CLI closure and exact application lock version ${peerVersions[name]}`);
185
+ const sdks = Object.entries(sdkDependencies).map(([name, version]) => {
186
+ const entry = realpathSync(entries.get(name)!), identity = packageIdentity(entry, name);
187
+ const location = relative(join(appRoot, 'node_modules'), identity.directory);
188
+ if (location.startsWith('..') || isAbsolute(location)) throw new Error(`${name}: customer SDK resolves outside the application installation`);
189
+ if (identity.version !== version || appLock.entry(name)?.version !== version) throw new Error(`${name}: customer SDK does not match its exact recipe and application lock version ${version}`);
190
+ return { package: name, version, entry: relative(appRoot, entry).split('\\').join('/') };
191
+ });
192
+ const application = { candidate: { package: metadata.name, version: candidate.version, integrity: options.artifactIntegrity }, kernel: { package: '@volter/world-core', version: kernel.version }, peers: peers.map(([name, identity]) => ({ package: name, version: identity.version })), sdks, dependencyLockSha256: createHash('sha256').update(appLockBytes).digest('hex'), appLocal: true };
142
193
  const commands: Array<{ label: string; argv: string[]; exitCode: number | null; stdout: string; stderr: string }> = [];
143
194
  const failures: string[] = [];
144
195
  const compiledCli = /\.[cm]?js$/.test(cliEntry);
145
196
  let nodeVersion: string | null = null;
146
197
  const execute = async (label: string, executable: string, argv: string[]) => {
147
- const result = await new Promise<{ exitCode: number | null; stdout: string; stderr: string }>((done) => {
148
- const child = spawn(executable, argv, { cwd: options.appDir, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
149
- let stdout = '', stderr = '';
150
- child.stdout.on('data', (chunk) => { stdout += chunk; });
151
- child.stderr.on('data', (chunk) => { stderr += chunk; });
152
- child.on('error', (error) => done({ exitCode: 1, stdout, stderr: `${stderr}\n${error.message}` }));
153
- child.on('close', (exitCode) => done({ exitCode, stdout, stderr }));
154
- });
198
+ const result = await runConsumerCommand([executable, ...argv], options.appDir, process.env);
155
199
  commands.push({ label, argv: [executable, ...argv], ...result });
156
200
  if (result.exitCode !== 0) throw new Error(`${label} exited ${result.exitCode}: ${result.stderr.trim().split('\n')[0] ?? ''}`);
157
201
  return result.stdout;
@@ -213,7 +257,7 @@ export async function runConsumerWorkflow(packDir: string, options: ConsumerOpti
213
257
  }
214
258
  }
215
259
  return {
216
- measured: true, passed: failures.length === 0 && teardownVerified,
260
+ measured: true as const, passed: failures.length === 0 && teardownVerified,
217
261
  scope: 'Exact installed artifact; normal CLI initialization, unchanged SDK, result assertions and retained-state read-only resume where applicable',
218
262
  package: metadata.name, version: metadata.version, integrity: options.artifactIntegrity,
219
263
  tools, application,
@@ -227,3 +271,21 @@ export async function runConsumerWorkflow(packDir: string, options: ConsumerOpti
227
271
  at: new Date().toISOString(),
228
272
  };
229
273
  }
274
+
275
+ /** The same installed execution can prove a declared SDK when the standard has no built-in client module. */
276
+ export function consumerClientProof(packDir: string, vendor: string, sdks: string[], result?: ConsumerResult): { failures: string[]; note?: string } {
277
+ if (!existsSync(join(packDir, 'journeys/first-use.json'))) return { failures: ['the pack supplies no journeys/first-use.json installed SDK workflow'] };
278
+ const recipe = consumerRecipe(vendor, packDir);
279
+ const dependencies = Object.entries(recipeDependencies(recipe)).filter(([name]) => sdks.includes(name));
280
+ if (!dependencies.length) return { failures: ['the installed workflow has no exact direct dependency named by adoption.sdks'] };
281
+ if (!result?.measured || !result.passed || result.failures.length || !result.teardownVerified) return { failures: ['the installed SDK workflow has no passing execution with verified teardown'] };
282
+ const metadata = JSON.parse(readFileSync(join(packDir, 'package.json'), 'utf8'));
283
+ const candidate = result.application.candidate;
284
+ if (result.package !== metadata.name || result.version !== metadata.version || candidate.package !== metadata.name || candidate.version !== metadata.version || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(result.integrity) || candidate.integrity !== result.integrity || result.application.appLocal !== true) return { failures: ['the installed SDK result does not identify this exact app-local candidate artifact'] };
285
+ if (result.fixture !== 'journeys/first-use.json' || result.sourceInputsSha256 !== consumerSourceDigest(packDir) || result.recipeSha256 !== createHash('sha256').update(JSON.stringify(recipe)).digest('hex')) return { failures: ['the installed SDK result does not describe the current behavior inputs and authored recipe'] };
286
+ for (const [name, version] of dependencies) {
287
+ const sdk = result.application.sdks.find((entry) => entry.package === name);
288
+ if (sdk?.version !== version || !sdk.entry.startsWith('node_modules/') || sdk.entry.split('/').includes('..')) return { failures: [`${name}: the installed SDK result has no exact app-local ${version} resolution`] };
289
+ }
290
+ return { failures: [], note: `installed customer workflow through ${dependencies.map(([name, version]) => `${name}@${version}`).join(', ')}; exact artifact and current recipe` };
291
+ }
@@ -0,0 +1,38 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import ts from 'typescript';
4
+
5
+ /** A literal property's effective value; a later spread or computed key cannot establish an import permission. */
6
+ function property(expression: ts.Expression | undefined, name: string): ts.Expression | undefined {
7
+ while (expression && (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression))) expression = expression.expression;
8
+ if (!expression || !ts.isObjectLiteralExpression(expression)) return undefined;
9
+ for (const member of [...expression.properties].reverse()) {
10
+ if (!ts.isPropertyAssignment(member) || ts.isComputedPropertyName(member.name)) return undefined;
11
+ if (member.name.text === name) return member.initializer;
12
+ }
13
+ return undefined;
14
+ }
15
+
16
+ /** The declared credential issuer's public subpaths, pinned as a direct runtime dependency. Reading the manifest as
17
+ * data keeps the source/tarball gate independent of dependency installation; typechecking verifies exported paths. */
18
+ export function credentialIssuerImports(unitDir: string, packageDir: string): (specifier: string) => boolean {
19
+ const path = join(unitDir, 'src', 'manifest.ts');
20
+ const packagePath = join(packageDir, 'package.json');
21
+ if (!existsSync(path) || !existsSync(packagePath)) return () => false;
22
+ const source = ts.createSourceFile(path, readFileSync(path, 'utf8'), ts.ScriptTarget.Latest, true);
23
+ const manifests = source.statements.flatMap(statement => ts.isVariableStatement(statement)
24
+ && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
25
+ ? statement.declarationList.declarations.filter(declaration => ts.isIdentifier(declaration.name) && declaration.name.text === 'manifest') : []);
26
+ const issuer = manifests.length === 1 ? property(property(manifests[0]!.initializer, 'descriptor'), 'credentialIssuer') : undefined;
27
+ if (!issuer || !ts.isStringLiteral(issuer) || !/^[a-z]/.test(issuer.text) || /[^a-z0-9-]/.test(issuer.text)) return () => false;
28
+ const packageName = `@volter/twin-${issuer.text}`;
29
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf8')) as { name?: string; dependencies?: Record<string, string> };
30
+ const version = pkg.dependencies?.[packageName];
31
+ const number = '(?:0|[1-9][0-9]*)';
32
+ const prerelease = '(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)';
33
+ const exactVersion = new RegExp(`^${number}\\.${number}\\.${number}(?:-${prerelease}(?:\\.${prerelease})*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$`);
34
+ if (pkg.name === packageName || typeof version !== 'string' || exactVersion.exec(version)?.[0] !== version) return () => false;
35
+ const prefix = `${packageName}/`;
36
+ return specifier => specifier.startsWith(prefix) && specifier.slice(prefix.length).split('/').every(segment =>
37
+ segment !== '' && segment !== '.' && segment !== '..' && !/[^A-Za-z0-9._-]/.test(segment) && segment.toLowerCase() !== 'node_modules');
38
+ }
package/src/derive.ts CHANGED
@@ -312,7 +312,7 @@ if (existsSync(manifestPath)) {
312
312
  // (a resource the spec names is read by its own list or retrieve; one the manifest names apart from the spec's, an RPC
313
313
  // vendor's, by the list or read the scope names, with `items` where the answer holds them when the spec does not say)
314
314
  const specNamed = new Set(ir.resources.map((r) => r.name));
315
- const scopes = Object.entries(manifest.resources as Record<string, { storedAs?: string; refresh?: { list?: string; get?: string; none?: string; items?: string; detail?: string } }>).flatMap(([name, decl]) => {
315
+ const scopes = Object.entries(manifest.resources as Record<string, { storedAs?: string; refresh?: { list?: string; get?: string; known?: boolean; none?: string; items?: string; detail?: string } }>).flatMap(([name, decl]) => {
316
316
  const r = decl.refresh;
317
317
  if (!r) return [];
318
318
  if ('graphql' in r && r.graphql) {
@@ -363,6 +363,8 @@ if (existsSync(manifestPath)) {
363
363
  if (r.get !== undefined) {
364
364
  const o = op(r.get);
365
365
  if (o?.class === 'retrieve' && ours(o)) return [];
366
+ // A known subject may be addressed by an id query whose native answer is a list envelope.
367
+ if (r.known && o?.class === 'list' && o.answers?.list && ours(o) && o.method.toUpperCase() === 'GET') return [];
366
368
  // a resource the spec does not name (a client-derived spec classes every request an action) is read by the GET
367
369
  // the scope names, the answer itself the subject
368
370
  if (o && !specNamed.has(name) && !o.resource && String(o.method).toUpperCase() === 'GET') return [];
package/src/gate.ts CHANGED
@@ -19,6 +19,7 @@ import ts from 'typescript';
19
19
  import { grade, type GradeOptions } from './grade.ts';
20
20
  import { withCheckoutFiles } from './protocol-3.ts';
21
21
  import { isPackArtifact } from './pack-files.ts';
22
+ import { credentialIssuerImports } from './credential-issuer-import.ts';
22
23
 
23
24
  /** Every unit of a pack repository: each pack (a directory with a package.json) and each lane in it (a directory with
24
25
  * its own src/manifest.ts and no package.json). */
@@ -102,6 +103,7 @@ export function laneRootFailures(dir: string): string[] {
102
103
  if (!/\bcreateVendorFetch\(/.test(src('fetch.ts'))) failures.push('layout: src/fetch.ts builds no createVendorFetch: the kernel routes a vendor\'s lanes from its manifest, nothing by hand');
103
104
  // the root's own files hold to the pack contract as a unit's do (the grade's kernel-contract)
104
105
  const beyond: string[] = [];
106
+ const issuerImport = credentialIssuerImports(dir, dir);
105
107
  for (const n of existsSync(join(dir, 'src')) ? readdirSync(join(dir, 'src')).filter((x) => /\.tsx?$/.test(x) && !x.endsWith('.d.ts')) : []) {
106
108
  // the file's imports as TypeScript reads them, as the grade's kernel-contract does
107
109
  for (const from of ts.preProcessFile(readFileSync(join(dir, 'src', n), 'utf8'), true, true).importedFiles.map((i) => i.fileName)) {
@@ -111,6 +113,7 @@ export function laneRootFailures(dir: string): string[] {
111
113
  if (/^\.\.\/[^/]+\/src\//.test(from) && n !== 'fetch.ts' && !laneData) { beyond.push(`src/${n} imports ${from}: only src/fetch.ts reaches a lane`); continue; }
112
114
  if (n === 'manifest.ts' && from !== '@volter/world-core') { beyond.push(`src/manifest.ts imports ${from}: the manifest is data`); continue; }
113
115
  if (from.startsWith('.') || from === '@volter/world-core' || from === '@volter/world-ui') continue;
116
+ if (issuerImport(from)) continue;
114
117
  if (n === 'cli.ts' && (from === '@volter/world-core/args' || from === '@volter/world-core/lifecycle')) continue;
115
118
  beyond.push(`src/${n} imports ${from}`);
116
119
  }
package/src/lanes.ts CHANGED
@@ -4,6 +4,13 @@
4
4
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
 
7
+ /** The pack and its manifest-bearing lanes, without crossing another package's boundary. */
8
+ export function unitsIn(pack: string): string[] {
9
+ if (!existsSync(pack)) return [];
10
+ return [pack, ...readdirSync(pack).sort().map((name) => join(pack, name))
11
+ .filter((directory) => !existsSync(join(directory, 'package.json')) && existsSync(join(directory, 'src', 'manifest.ts')))];
12
+ }
13
+
7
14
  /** Metadata such as sources.json is not a derivable API of a vendor whose APIs all belong to its lanes. */
8
15
  export function hasOwnSurface(pkg: string): boolean {
9
16
  if (existsSync(join(pkg, 'src', 'generated', 'surface.gen.json'))) return true;
package/src/protocol-3.ts CHANGED
@@ -17,6 +17,7 @@ import ts from 'typescript';
17
17
  import { fileURLToPath } from 'node:url';
18
18
  import { sharingLanesIn } from './lanes.ts';
19
19
  import { isPackArtifact } from './pack-files.ts';
20
+ import { credentialIssuerImports } from './credential-issuer-import.ts';
20
21
  import type { Criterion, GradeUnit, Standard } from './types.ts';
21
22
 
22
23
  // A path relative to another with '/' between its parts on every platform: git lists files that way, and every rule
@@ -340,13 +341,14 @@ const criteria: Criterion[] = [
340
341
  },
341
342
  {
342
343
  id: 'handler-contract', section: 'form',
343
- asks: 'every file in semantics/ reaches no randomness, clock, environment, network or module but the kernel, the states and the shared helpers; each handler file is its family\'s and exports only its operations, named by the fixed rule',
344
+ asks: 'every file in semantics/ reaches no randomness, clock, environment, network or module but the kernel, the states, the shared helpers and a public subpath of its declared credential issuer pinned at an exact version as a direct runtime dependency; each handler file is its family\'s and exports only its operations, named by the fixed rule',
344
345
  check(unit) {
345
346
  const dir = join(unit.dir, 'src', 'semantics');
346
347
  if (!existsSync(dir)) return pass('no handlers');
347
348
  const ops = operationsOf(unit);
348
349
  const families = new Set(ops.values());
349
350
  const found: string[] = [];
351
+ const issuerImport = credentialIssuerImports(unit.dir, packageOf(unit));
350
352
  for (const f of filesUnder(dir).filter((x) => /\.tsx?$/.test(x) && !TEST_FILE.test(x))) {
351
353
  const text = stripComments(readFileSync(join(dir, f), 'utf8'));
352
354
  const raw = readFileSync(join(dir, f), 'utf8');
@@ -363,7 +365,7 @@ const criteria: Criterion[] = [
363
365
  const engineModule = /^\.\.\/engine\/[A-Za-z0-9_-]+\.ts$/.test(from) || vendorPath(unit, from, 3, /^engine\/[A-Za-z0-9_-]+\.ts$/);
364
366
  // a lane over its vendor's state takes the vendor's states and shared helpers ("Pack layout")
365
367
  const vendorShared = vendorPath(unit, from, 3, /^(semantics\/(states|shared)|manifest)\.ts$/);
366
- if (from !== '@volter/world-core' && !/^\.\/(states|shared)\.ts$/.test(from) && !mapImport && !engineModule && !vendorShared) found.push(`${f} imports ${from}`);
368
+ if (from !== '@volter/world-core' && !/^\.\/(states|shared)\.ts$/.test(from) && !mapImport && !engineModule && !vendorShared && !issuerImport(from)) found.push(`${f} imports ${from}`);
367
369
  }
368
370
  // the handler map is typed over the narrow context, so the compiler holds every handler it names to the contract
369
371
  if (f === 'index.ts' && !/:\s*Record<\s*string\s*,\s*Handler\s*>/.test(text)) found.push('index.ts does not type its handler map Record<string, Handler>');
@@ -468,11 +470,12 @@ const criteria: Criterion[] = [
468
470
  },
469
471
  {
470
472
  id: 'kernel-contract', section: 'form',
471
- asks: "every file of src/ imports the kernel through its root, the pack contract, and nothing past it: no `@volter/world-core/runtime` or other entry (cli.ts alone takes args and lifecycle), no node builtin, no other package but @volter/world-ui and the generated GraphQL library loader",
473
+ asks: "every file of src/ imports the kernel through its root, the pack contract, and nothing past it: no `@volter/world-core/runtime` or other entry (cli.ts alone takes args and lifecycle), no node builtin, no other package but @volter/world-ui, a public subpath of its declared credential issuer pinned at an exact version as a direct runtime dependency, and the generated GraphQL library loader",
472
474
  check(unit) {
473
475
  const src = join(unit.dir, 'src');
474
476
  if (!existsSync(src)) return fail('src/ is missing');
475
477
  const found: string[] = [];
478
+ const issuerImport = credentialIssuerImports(unit.dir, packageOf(unit));
476
479
  for (const f of filesUnder(src).filter((x) => /\.tsx?$/.test(x) && !TEST_FILE.test(x) && !x.startsWith('generated/'))) {
477
480
  // the file's imports, re-exports and dynamic imports as TypeScript reads them (a string that only reads like one,
478
481
  // an error message's "from '<name>'", is none)
@@ -512,6 +515,7 @@ const criteria: Criterion[] = [
512
515
  for (const from of specifiers) {
513
516
  if (from.startsWith('.')) continue;
514
517
  if (from === '@volter/world-core' || from === '@volter/world-ui') continue;
518
+ if (issuerImport(from)) continue;
515
519
  if (f === 'cli.ts' && (from === '@volter/world-core/args' || from === '@volter/world-core/lifecycle')) continue;
516
520
  found.push(`${f} imports ${from}`);
517
521
  }
@@ -10,24 +10,113 @@ import { parse as parseYaml } from 'yaml';
10
10
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
12
  import { gunzipSync } from 'node:zlib';
13
+ import { applySpecPatch, type SpecPatch } from './spec-patches.ts';
14
+ import { slug } from './spec-ir.ts';
13
15
 
14
16
  type Json = any;
15
17
  export type SpecDocuments = { documents: Record<string, Json> };
16
18
 
19
+ function readDocument(file: string): Json {
20
+ const bytes = readFileSync(file);
21
+ const text = (file.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('utf8');
22
+ return /\.ya?ml(\.gz)?$/.test(file) ? parseExactJson(JSON.stringify(parseYaml(text, { intAsBigInt: true }), (_, value) => typeof value === 'bigint' ? exactInteger(String(value)) : value)) : parseExactJson(text);
23
+ }
24
+
17
25
  /** The documents of `spec/openapi/`, by name, or undefined when the pack vendors one spec file. */
18
26
  export function readSpecDocuments(specDir: string): SpecDocuments | undefined {
19
27
  const dir = join(specDir, 'openapi');
20
28
  if (!existsSync(dir)) return undefined;
21
29
  const documents: Record<string, Json> = {};
22
30
  for (const f of readdirSync(dir).filter((f) => /\.(json|ya?ml)(\.gz)?$/.test(f)).sort()) {
23
- const bytes = readFileSync(join(dir, f));
24
- const text = (f.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('utf8');
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);
31
+ documents[f.replace(/\.(json|ya?ml)(\.gz)?$/, '')] = readDocument(join(dir, f));
26
32
  }
27
33
  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}`);
28
34
  return { documents };
29
35
  }
30
36
 
37
+ const openapiFiles = ['openapi.json.gz', 'openapi.json', 'openapi.yaml.gz', 'openapi.yaml'];
38
+
39
+ function readPatchedDocument(specDir: string, filenames: string[]): Json | undefined {
40
+ const file = filenames.map((name) => join(specDir, name)).find(existsSync);
41
+ const documents = file === undefined ? readSpecDocuments(specDir) : undefined;
42
+ let doc = file === undefined ? documents : readDocument(file);
43
+ if (doc === undefined) return undefined;
44
+ const patches = join(specDir, 'patches.json');
45
+ if (existsSync(patches)) for (const patch of (readDocument(patches) as { patches: SpecPatch[] }).patches) applySpecPatch(doc, patch);
46
+ if (documents !== undefined) {
47
+ const merged = mergeDocuments(doc);
48
+ if (merged.contradictions.length) throw new Error(`OpenAPI response declarations conflict: ${merged.contradictions.join('; ')}`);
49
+ doc = merged.doc;
50
+ }
51
+ return doc;
52
+ }
53
+
54
+ /** Response declarations as served, independent of their representation media or generated surface version. */
55
+ export function declaredResponseStatuses(specDir: string): Map<string, Set<string>> {
56
+ const doc = readPatchedDocument(specDir, openapiFiles);
57
+ const statuses = new Map<string, Set<string>>();
58
+ if (doc === undefined) return statuses;
59
+ for (const [path, item] of Object.entries(doc.paths ?? {}) as Array<[string, Record<string, { operationId?: string; responses?: Record<string, unknown> }>]>) {
60
+ for (const [method, operation] of Object.entries(item)) {
61
+ if (!operation || typeof operation !== 'object' || !operation.responses) continue;
62
+ statuses.set(operation.operationId || slug(method, path), new Set(Object.keys(operation.responses)));
63
+ }
64
+ }
65
+ return statuses;
66
+ }
67
+
68
+ /** Declared schema fields, including nested objects that are not inferred as generated resources. */
69
+ export function schemaFieldNames(specDir: string): Set<string> {
70
+ const doc = readPatchedDocument(specDir, ['smithy.json.gz', 'smithy.json', ...openapiFiles, 'discovery.json.gz', 'discovery.json']);
71
+ const fields = new Set<string>();
72
+ if (doc === undefined) return fields;
73
+ for (const shape of Object.values(doc.shapes ?? {}) as Json[]) {
74
+ if (shape.type !== 'structure' && shape.type !== 'union') continue;
75
+ for (const [name, member] of Object.entries(shape.members ?? {}) as Array<[string, Json]>) {
76
+ fields.add(name);
77
+ for (const trait of ['smithy.api#jsonName', 'smithy.api#xmlName']) {
78
+ const wireName = member.traits?.[trait];
79
+ if (typeof wireName === 'string') fields.add(wireName);
80
+ }
81
+ }
82
+ }
83
+ const schema = (node: Json): void => {
84
+ if (Array.isArray(node)) { for (const item of node) schema(item); return; }
85
+ if (!node || typeof node !== 'object') return;
86
+ for (const [name, property] of Object.entries(node.properties ?? {}) as Array<[string, Json]>) {
87
+ fields.add(name);
88
+ if (typeof property?.xml?.name === 'string') fields.add(property.xml.name);
89
+ schema(property);
90
+ }
91
+ for (const keyword of ['$defs', 'definitions', 'patternProperties', 'dependentSchemas']) {
92
+ for (const child of Object.values(node[keyword] ?? {})) schema(child);
93
+ }
94
+ for (const keyword of ['items', 'additionalItems', 'additionalProperties', 'unevaluatedProperties', 'unevaluatedItems', 'not', 'contains', 'propertyNames', 'if', 'then', 'else', 'allOf', 'anyOf', 'oneOf', 'prefixItems']) schema(node[keyword]);
95
+ };
96
+ for (const catalog of [doc.components?.schemas, doc.definitions, doc.schemas]) {
97
+ for (const node of Object.values(catalog ?? {})) schema(node);
98
+ }
99
+ const body = (node: Json): void => {
100
+ schema(node?.schema);
101
+ for (const media of Object.values(node?.content ?? {}) as Json[]) schema(media?.schema);
102
+ };
103
+ for (const catalog of [doc.components?.responses, doc.components?.requestBodies, doc.components?.parameters, doc.components?.headers, doc.responses, doc.parameters]) {
104
+ for (const node of Object.values(catalog ?? {})) body(node);
105
+ }
106
+ for (const paths of [doc.paths, doc.webhooks, doc.components?.pathItems]) {
107
+ for (const item of Object.values(paths ?? {}) as Json[]) {
108
+ for (const parameter of item.parameters ?? []) body(parameter);
109
+ for (const method of ['get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace']) {
110
+ const operation = item[method];
111
+ for (const parameter of operation?.parameters ?? []) body(parameter);
112
+ body(operation?.requestBody);
113
+ for (const response of Object.values(operation?.responses ?? {})) body(response);
114
+ }
115
+ }
116
+ }
117
+ return fields;
118
+ }
119
+
31
120
  /** A node without its words (description, summary, examples, and the tags a document files it under): what two
32
121
  * documents must agree on. */
33
122
  const substance = (n: Json): string => JSON.stringify(n, (k, v) => (['description', 'summary', 'example', 'examples', 'x-mint', 'tags'].includes(k) ? undefined : v && typeof v === 'object' && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort(([a], [b]) => a.localeCompare(b))) : v));
@@ -6,9 +6,9 @@ import { parseExactJson } from '@volter/world-core/exact-json';
6
6
  // is its precondition, not what it measures. This module is no test file itself, so a published package holds it.
7
7
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
- import { listActions, observeTransitions, stateOf, type Actor, type DerivedManifest, type Transition } from '@volter/world-core/runtime';
9
+ import { listActions, observeTransitions, stateOf, type Actor, type DerivedManifest, type ErrorSpec, type Transition } from '@volter/world-core/runtime';
10
10
  import { inMemoryWorld, walk, type Failure, usePacks } from '../walk/behavior-journey.ts';
11
- import { lanesOf, shapeChecker } from '../walk/shape.ts';
11
+ import { lanesOf, lineShape, shapeChecker } from '../walk/shape.ts';
12
12
  import { sharingLanesIn } from '../lanes.ts';
13
13
 
14
14
  /** Registers, in the `bun test` run that imports it, the walks of `vendor` in the pack repository at `PACKS`. */
@@ -30,9 +30,12 @@ for (const [n, w] of walks.entries()) test(`${vendor}: ${w.file} walks`, async (
30
30
  const { manifest } = (await import(join(PACKS, vendor, 'src', 'manifest.ts'))) as { manifest: DerivedManifest };
31
31
  // the lanes over the vendor's state its serve factory serves are walked by this life, their machines with the vendor's
32
32
  // ("Pack layout"); two manifests naming one resource's field would collide in the keys below, so that refuses
33
- const laneManifests = await Promise.all(sharingLanes(vendor).map(async (l) => ((await import(join(PACKS, vendor, l, 'src', 'manifest.ts'))) as { manifest: DerivedManifest }).manifest));
33
+ const units = [{ dir: join(PACKS, vendor), manifest, lane: undefined }, ...await Promise.all(sharingLanes(vendor).map(async (l) => {
34
+ const dir = join(PACKS, vendor, l);
35
+ return { dir, lane: l, manifest: ((await import(join(dir, 'src', 'manifest.ts'))) as { manifest: DerivedManifest }).manifest };
36
+ }))];
34
37
  // a vendor of lanes' root (a VendorManifest) declares no resources of its own: its lanes do
35
- const allResources = [manifest, ...laneManifests].flatMap((mf) => Object.entries(mf.resources ?? {}));
38
+ const allResources = units.flatMap(({ manifest: mf }) => Object.entries(mf.resources ?? {}));
36
39
  const machines = new Map<string, unknown>();
37
40
  for (const [resource, decl] of allResources) for (const [field, machine] of Object.entries(decl.state ?? {})) {
38
41
  const name = `${resource}.${field}`;
@@ -42,20 +45,25 @@ for (const [n, w] of walks.entries()) test(`${vendor}: ${w.file} walks`, async (
42
45
  machines.set(name, machine);
43
46
  }
44
47
  const where = new Map<Transition, Array<{ at: string; field: string; type: string; service: string }>>();
45
- for (const mf of [manifest, ...laneManifests]) for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
46
- for (const [field, m] of Object.entries(decl.state ?? {})) m.transitions.forEach((t, i) => {
47
- const place = { at: `${resource}.${field}#${i}`, field, type: decl.storedAs ?? resource, service: mf.service ?? mf.vendor };
48
- const places = where.get(t) ?? [];
49
- if (!places.some((p) => p.at === place.at && p.type === place.type && p.service === place.service)) where.set(t, [...places, place]);
50
- });
48
+ const httpTransitions = new Set<Transition>();
49
+ for (const { dir, manifest: mf } of units) {
50
+ const http = !lineShape(dir);
51
+ for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
52
+ for (const [field, m] of Object.entries(decl.state ?? {})) m.transitions.forEach((t, i) => {
53
+ if (http) httpTransitions.add(t);
54
+ const place = { at: `${resource}.${field}#${i}`, field, type: decl.storedAs ?? resource, service: mf.service ?? mf.vendor };
55
+ const places = where.get(t) ?? [];
56
+ if (!places.some((p) => p.at === place.at && p.type === place.type && p.service === place.service)) where.set(t, [...places, place]);
57
+ });
58
+ }
51
59
  }
52
60
  const moves = new Map<string, Array<{ transition: Transition; from: string; field: string; actor: Actor; subject: string }>>();
53
61
  const subjectKey = (service: string, type: string, id: string): string => JSON.stringify([service, type, id]);
54
62
  observeTransitions((_decl, t, outcome, from, entry) => {
55
63
  const places = where.get(t) ?? [];
56
64
  if (outcome === 'refused') {
57
- // A failed check on an already committed write is no refusal the application received.
58
- if (entry) return;
65
+ // HTTP refusals are credited from their received status and body; a line protocol owns its session's replies.
66
+ if (entry || httpTransitions.has(t)) return;
59
67
  const groups = new Set(places.map((p) => JSON.stringify([p.service, p.type, p.field])));
60
68
  if (groups.size === 1) for (const p of places) seen.add(`${p.at}|refused|${from}`);
61
69
  return;
@@ -79,12 +87,14 @@ for (const [n, w] of walks.entries()) test(`${vendor}: ${w.file} walks`, async (
79
87
  const root = world.root();
80
88
  // what each step was answered, by the spec operation it called: a refusal is seen at the wire, whichever code gave it
81
89
  const shape = await shapeChecker(join(PACKS, vendor));
82
- const answered: Array<{ op: string; text: string; body: unknown }> = [];
90
+ const answered: Array<{ op: string; lane?: string; status: number; body: unknown }> = [];
83
91
  try {
84
- await walk(life, root, failures, { beats: w.beats, hooks: { after: (step, path, answer) => {
92
+ await walk(life, root, failures, { beats: w.beats, hooks: { after: (_step, _path, answer) => {
85
93
  // a line step's refusals are the machine's own (observeTransitions); a request's are read off its answer
86
- const op = 'method' in step ? shape?.operationOf(step.method, path, { ...(life.headers ?? {}), ...(step.headers ?? {}) })?.op.id : undefined;
87
- if (op) answered.push({ op, text: JSON.stringify(answer.body ?? ''), body: answer.body });
94
+ const request = answer.request;
95
+ if (!request) return;
96
+ const hit = shape?.operationOf(request.method, request.url, request.headers);
97
+ if (hit) answered.push({ op: hit.op.id, lane: hit.lane, status: answer.status, body: answer.wireBody });
88
98
  } } });
89
99
  // a declared refusal is reached when a step on its operation was answered with it: its code, or its message
90
100
  // up to the first placeholder
@@ -92,13 +102,13 @@ for (const [n, w] of walks.entries()) test(`${vendor}: ${w.file} walks`, async (
92
102
  const holds = (value: unknown, code: string): boolean => (value !== null && typeof value === 'object'
93
103
  ? Object.values(value as Record<string, unknown>).some((v) => holds(v, code))
94
104
  : (typeof value === 'string' || typeof value === 'number') && String(value) === code);
95
- const answers = (a: { text: string; body: unknown }, r: { code?: string | number; message: string }): boolean =>
96
- r.code !== undefined ? holds(a.body, String(r.code)) : a.text.includes(r.message.split('{')[0]!.trim());
97
- for (const [resource, decl] of allResources) {
105
+ const answers = (a: { status: number; body: unknown }, r: ErrorSpec): boolean =>
106
+ a.status === r.status && (r.code !== undefined ? holds(a.body, String(r.code)) : JSON.stringify(a.body)?.includes(r.message.split('{')[0]!.trim()) === true);
107
+ for (const { manifest: mf, lane } of units) for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
98
108
  for (const [field, m] of Object.entries(decl.state ?? {})) {
99
109
  m.transitions.forEach((t, i) => {
100
110
  if (!t.operation) return;
101
- const on = answered.filter((a) => a.op === t.operation);
111
+ const on = answered.filter((a) => a.op === t.operation && a.lane === lane);
102
112
  if (t.refusal && on.some((a) => answers(a, t.refusal!))) seen.add(`${resource}.${field}#${i}|refused|*`);
103
113
  for (const [from, r] of Object.entries(t.refusals ?? {})) if (on.some((a) => answers(a, r))) seen.add(`${resource}.${field}#${i}|refused|${from}`);
104
114
  });
@@ -108,13 +118,13 @@ for (const [n, w] of walks.entries()) test(`${vendor}: ${w.file} walks`, async (
108
118
  // transition that allows it (from its state to the next, by its operation when both name one); a change no
109
119
  // transition allows is an undeclared move
110
120
  const fieldsOf = new Map<string, Array<[string, string, (typeof manifest.resources)[string]['state'] extends infer S ? S extends Record<string, infer F> ? F : never : never]>>();
111
- for (const mf of [manifest, ...laneManifests]) for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
121
+ for (const { manifest: mf } of units) for (const [resource, decl] of Object.entries(mf.resources ?? {})) {
112
122
  const key = JSON.stringify([mf.service ?? mf.vendor, decl.storedAs ?? resource]);
113
123
  for (const [field, m] of Object.entries(decl.state ?? {})) fieldsOf.set(key, [...(fieldsOf.get(key) ?? []), [resource, field, m]]);
114
124
  }
115
125
  const current = new Map<string, Record<string, unknown>>();
116
126
  // the services the life wrote: the vendor's, or its lanes' (a vendor of lanes' root names none of its own)
117
- const services = [...new Set([manifest, ...laneManifests].map((mf) => mf.service).filter((x): x is string => typeof x === 'string'))];
127
+ const services = [...new Set(units.map(({ manifest: mf }) => mf.service).filter((x): x is string => typeof x === 'string'))];
118
128
  if (!services.length) services.push(manifest.vendor);
119
129
  for (const a of services.flatMap((service) => listActions(service, root))) {
120
130
  if (a.op !== 'set' || !a.fields) continue;
@@ -3,7 +3,7 @@
3
3
  // life per pack is walked through the pack's own fetch on the in-memory World, and the report says:
4
4
  // - the life: whether a judge's plausible verdict names its current content (a verdict its author wrote is not one);
5
5
  // - correctness: every check on every step (status, expectations, captures, SHAPE, the beats) and the life
6
- // answering byte for byte the same on a second fresh World;
6
+ // answering the same on a second fresh World under its declared engine outputs;
7
7
  // - coverage: the hand-written code the life and the vendor's published examples run and the declared state logic
8
8
  // they reach (life-coverage.ts), with what they never reach;
9
9
  // - the vendor's published examples: replayed and judged (vendor-examples.ts);
@@ -27,6 +27,7 @@ import { dirname, join, sep } from 'node:path';
27
27
  import { inMemoryWorld, walk, withPinnedWorldDraws, type Failure, usePacks } from '../walk/behavior-journey.ts';
28
28
  import { expandFills, waitMs, type Journey } from '../walk/journey-kit.ts';
29
29
  import { vendorFetch, vendorStream } from '../walk/vendor-fetch.ts';
30
+ import { compareReplays, type ReplayStep } from '../walk/replay.ts';
30
31
  import { existenceProbe } from '../walk/journey-kit.ts';
31
32
  import { kernelCheckout, packsRoot, toolPath } from './packs-root.ts';
32
33
  const PACKS = packsRoot();
@@ -73,10 +74,10 @@ const judged = life.steps.length === 0 || !verdict ? 'unjudged' : dependent ? de
73
74
  const world = inMemoryWorld(`score-${vendor}`);
74
75
  const failures: Failure[] = [];
75
76
  await walk(life, world.root(), failures, { beats: true });
76
- const answers = async (): Promise<string[]> => {
77
- const seen: string[] = [];
77
+ const answers = async (): Promise<ReplayStep[]> => {
78
+ const seen: ReplayStep[] = [];
78
79
  // the World's own draws (its seed, its keys) are the same in both: two Worlds differ only by what is beyond them (R9)
79
- await withPinnedWorldDraws(() => walk(life, world.root(), [], { beats: false, hooks: { after: (step, p, a) => seen.push(`${(step as { name?: string }).name ?? ''}\u0000${p} ${a.status} ${JSON.stringify(a.body)}`) } }));
80
+ await withPinnedWorldDraws(() => walk(life, world.root(), failures, { beats: false, hooks: { after: (step, path, answer) => seen.push({ name: step.name ?? '', path, status: answer.status, body: answer.body, ...(answer.sql ? { sql: answer.sql } : {}), ...(answer.connection ? { connection: answer.connection } : {}) }) } }));
80
81
  return seen;
81
82
  };
82
83
  const [first, second] = [await answers(), await answers()];
@@ -166,8 +167,8 @@ const flags: Flag[] = [];
166
167
  }
167
168
  }
168
169
  }
169
- const differs = first.findIndex((line, i) => line !== second[i]);
170
- if (differs >= 0 || first.length !== second.length) failures.push({ step: '(whole life)', beat: 'REPLAYABLE', detail: `a second fresh World answered differently at "${(first[differs] ?? second[differs] ?? first.at(-1) ?? '').split('\u0000')[0]}" (the ${differs + 1}th answered step; waits are not counted)` });
170
+ const replayComparison = compareReplays(first, second);
171
+ for (const detail of replayComparison.diagnostics) failures.push({ step: '(whole life)', beat: 'REPLAYABLE', detail });
171
172
 
172
173
  // ── breadth ──────────────────────────────────────────────────────────────────────────────────
173
174
  const factory = await vendorFetch(PACKS, vendor);