@volter/twin-standard 1.0.103 → 1.0.104

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.
package/README.md CHANGED
@@ -28,8 +28,12 @@ The public `assessPack(packDir, { browser: true, consumer })` API runs inside a
28
28
  kernel dispatch and replays journeys through Chromium when their wire is HTTP. Install Playwright and Chromium for
29
29
  that target. Its report distinguishes API support, operations exercised, journey failures and replay, and does not
30
30
  claim DOM interaction coverage. `consumer` contains absolute `appDir` and `cli` paths and the candidate's
31
- `artifactIntegrity`. Prepare the pack's `journeys/first-use.json` with `prepareConsumerApp(packDir, newAppDir)`,
32
- then install its exact SDK dependencies with scripts disabled before the assessment World starts. The public
31
+ `artifactIntegrity`. Prepare the pack's `journeys/first-use.json` with `prepareConsumerApp(packDir, newAppDir, { cli, artifact })`:
32
+ `artifact` is the absolute unpublished tarball path, or omitted for a registry release. Preparation adds the exact candidate
33
+ and the CLI's kernel peer to the app. Install those and its exact SDK dependencies with scripts disabled before the
34
+ assessment World starts. The journey refuses candidate or kernel imports from evaluator ancestors and checks the
35
+ app's own lockfile against the assessed integrity. Older evidence without this installation identity does not satisfy
36
+ the current completion check. The public
33
37
  `runConsumerWorkflow(packDir, consumer)` lets a builder walk only that entry journey while authoring; it runs no
34
38
  grade or conformance checks. Installed readiness requires ordinary CLI initialization against the exact candidate,
35
39
  SDK assertions, read-only state inspection after stop/resume for stored workflows, and verified teardown. A
@@ -67,6 +67,19 @@ export declare function assessPack(packDir: string, options?: AssessmentOptions)
67
67
  '@volter/world-core': string;
68
68
  '@volter/world-runtime': string;
69
69
  };
70
+ application: {
71
+ candidate: {
72
+ package: any;
73
+ version: string;
74
+ integrity: string;
75
+ };
76
+ kernel: {
77
+ package: string;
78
+ version: string;
79
+ };
80
+ dependencyLockSha256: string;
81
+ appLocal: boolean;
82
+ };
70
83
  sourceInputsSha256: string;
71
84
  recipeSha256: string;
72
85
  fixture: string;
package/dist/src/cli.js CHANGED
@@ -31,7 +31,7 @@ import { standardPackageFile } from "./package-file.js";
31
31
  import { gatePublished } from "./published.js";
32
32
  const [command, ...args] = process.argv.slice(2);
33
33
  const usage = () => {
34
- console.error('usage: twin-standard gate <repo> [units…] | gate-published <name>@<version>… [--registry <url>] | grade <unit>… [--static] [--form] | derive <unit> [--out <dir>] | read-page <url> [--raw] | check-sources <unit> [--recheck] | conform <pack dir> | <name>@<version> [--registry <url>] | create <dir> --init <vendor> --package <@scope/name> | create <unit> --index | facts <pack dir> [--out <file>] | prepare-consumer <pack dir> --app <new dir> --cli <volter executable> --integrity <sha512> --out <json> | assess <pack dir> --prepared-consumer <json> [--browser] [--out <file>] | --version');
34
+ console.error('usage: twin-standard gate <repo> [units…] | gate-published <name>@<version>… [--registry <url>] | grade <unit>… [--static] [--form] | derive <unit> [--out <dir>] | read-page <url> [--raw] | check-sources <unit> [--recheck] | conform <pack dir> | <name>@<version> [--registry <url>] | create <dir> --init <vendor> --package <@scope/name> | create <unit> --index | facts <pack dir> [--out <file>] | prepare-consumer <pack dir> --app <new dir> --cli <volter executable> --integrity <sha512> --out <json> [--candidate <tgz>] | assess <pack dir> --prepared-consumer <json> [--browser] [--out <file>] | --version');
35
35
  process.exit(2);
36
36
  };
37
37
  if (command === '--version') {
@@ -123,10 +123,12 @@ else if (command === 'prepare-consumer') {
123
123
  if (existsSync(out))
124
124
  throw new Error('Prepared consumer output already exists');
125
125
  const { prepareConsumerApp } = await import("./consumer.js");
126
- const fixture = prepareConsumerApp(resolve(args[0]), appDir);
126
+ const candidateAt = args.indexOf('--candidate');
127
+ const artifact = candidateAt >= 0 ? resolve(value('--candidate')) : undefined;
128
+ const fixture = prepareConsumerApp(resolve(args[0]), appDir, { cli, artifact });
127
129
  const { writeFileSync } = await import('node:fs');
128
130
  writeFileSync(out, JSON.stringify({ appDir, cli, artifactIntegrity }, null, 2) + '\n');
129
- console.log(JSON.stringify({ appDir, fixture, preparedConsumer: out, next: 'Install the app SDK dependencies with scripts disabled before assessing inside a World' }));
131
+ console.log(JSON.stringify({ appDir, fixture, preparedConsumer: out, next: 'Install the app SDK, candidate and kernel dependencies with scripts disabled before assessing inside a World' }));
130
132
  }
131
133
  else if (command === 'assess') {
132
134
  if (!args[0])
@@ -19,12 +19,16 @@ export type ConsumerOptions = {
19
19
  cli: string;
20
20
  artifactIntegrity: string;
21
21
  };
22
+ type ConsumerInstallation = {
23
+ cli: string;
24
+ artifact?: string;
25
+ };
22
26
  /** Bind behavior inputs on both source and packed layouts; compiled outputs and score caches are not inputs. */
23
27
  export declare function consumerSourceDigest(pack: string): string;
24
28
  /** A pack supplies its own entry journey; older immutable releases use a versioned standard fixture. */
25
29
  export declare function consumerRecipe(vendor: string, packDir?: string): ConsumerRecipe;
26
30
  /** Materialize only a new caller-owned app. Normal dependency installation happens before offline assessment. */
27
- export declare function prepareConsumerApp(packDir: string, appDir: string): {
31
+ export declare function prepareConsumerApp(packDir: string, appDir: string, installation?: ConsumerInstallation): {
28
32
  recipeSha256: string;
29
33
  dependencies: Record<string, string>;
30
34
  };
@@ -47,6 +51,19 @@ export declare function runConsumerWorkflow(packDir: string, options: ConsumerOp
47
51
  '@volter/world-core': string;
48
52
  '@volter/world-runtime': string;
49
53
  };
54
+ application: {
55
+ candidate: {
56
+ package: any;
57
+ version: string;
58
+ integrity: string;
59
+ };
60
+ kernel: {
61
+ package: string;
62
+ version: string;
63
+ };
64
+ dependencyLockSha256: string;
65
+ appLocal: boolean;
66
+ };
50
67
  sourceInputsSha256: string;
51
68
  recipeSha256: string;
52
69
  fixture: string;
@@ -69,3 +86,4 @@ export declare function runConsumerWorkflow(packDir: string, options: ConsumerOp
69
86
  mode: string;
70
87
  };
71
88
  }>;
89
+ export {};
@@ -2,10 +2,33 @@
2
2
  import { createHash } from 'node:crypto';
3
3
  import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
4
4
  import { createRequire } from 'node:module';
5
- import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
5
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
6
6
  import { spawn } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { standardPackageFile } from "./package-file.js";
9
+ function packageIdentity(entry, name) {
10
+ let directory = dirname(realpathSync(entry));
11
+ while (true) {
12
+ const file = join(directory, 'package.json');
13
+ if (existsSync(file)) {
14
+ const pkg = JSON.parse(readFileSync(file, 'utf8'));
15
+ if (pkg.name === name)
16
+ return { directory, version: pkg.version };
17
+ }
18
+ const parent = dirname(directory);
19
+ if (parent === directory)
20
+ throw new Error(`${name} has no installed package identity`);
21
+ directory = parent;
22
+ }
23
+ }
24
+ function consumerTools(cli) {
25
+ const entry = realpathSync(cli), require = createRequire(entry);
26
+ return {
27
+ '@volter/world': packageIdentity(entry, '@volter/world').version,
28
+ '@volter/world-core': packageIdentity(require.resolve('@volter/world-core/runtime'), '@volter/world-core').version,
29
+ '@volter/world-runtime': packageIdentity(require.resolve('@volter/world-runtime'), '@volter/world-runtime').version,
30
+ };
31
+ }
9
32
  /** Bind behavior inputs on both source and packed layouts; compiled outputs and score caches are not inputs. */
10
33
  export function consumerSourceDigest(pack) {
11
34
  const entries = [];
@@ -49,7 +72,7 @@ export function consumerRecipe(vendor, packDir) {
49
72
  return recipe;
50
73
  }
51
74
  /** Materialize only a new caller-owned app. Normal dependency installation happens before offline assessment. */
52
- export function prepareConsumerApp(packDir, appDir) {
75
+ export function prepareConsumerApp(packDir, appDir, installation) {
53
76
  const facts = JSON.parse(readFileSync(join(packDir, 'generated/pack-facts.json'), 'utf8'));
54
77
  const vendor = Object.keys(facts.packs ?? {})[0];
55
78
  if (!vendor || Object.keys(facts.packs).length !== 1)
@@ -71,6 +94,16 @@ export function prepareConsumerApp(packDir, appDir) {
71
94
  throw new Error('Consumer files must contain text');
72
95
  return [path, content];
73
96
  });
97
+ if (installation) {
98
+ const metadata = JSON.parse(readFileSync(join(packDir, 'package.json'), 'utf8'));
99
+ const tools = consumerTools(installation.cli);
100
+ if (installation.artifact && (!isAbsolute(installation.artifact) || !statSync(installation.artifact).isFile()))
101
+ throw new Error('Consumer candidate must be an absolute prepared archive path');
102
+ if (Object.hasOwn(app.dependencies ?? {}, metadata.name))
103
+ throw new Error('Consumer SDK dependencies must not substitute the candidate package');
104
+ app.devDependencies = { ...app.devDependencies, '@volter/world-core': tools['@volter/world-core'], [metadata.name]: installation.artifact ? `file:${installation.artifact}` : metadata.version };
105
+ files.find(([path]) => path === 'package.json')[1] = JSON.stringify(app, null, 2) + '\n';
106
+ }
74
107
  mkdirSync(appDir, { recursive: true });
75
108
  for (const [path, content] of files) {
76
109
  mkdirSync(dirname(join(appDir, path)), { recursive: true });
@@ -83,29 +116,24 @@ export async function runConsumerWorkflow(packDir, options) {
83
116
  throw new Error('Consumer requires absolute prepared paths and candidate SHA-512 integrity');
84
117
  if (existsSync(join(options.appDir, '.volter/world.json')))
85
118
  throw new Error('Consumer app already has a World; prepare a new app rather than reusing prior state');
86
- const packageVersion = (entry, name) => {
87
- let directory = dirname(entry);
88
- while (true) {
89
- const file = join(directory, 'package.json');
90
- if (existsSync(file)) {
91
- const pkg = JSON.parse(readFileSync(file, 'utf8'));
92
- if (pkg.name === name)
93
- return pkg.version;
94
- }
95
- const parent = dirname(directory);
96
- if (parent === directory)
97
- throw new Error(`${name} has no installed package identity`);
98
- directory = parent;
99
- }
100
- };
101
119
  const cliEntry = realpathSync(options.cli);
102
- const cliRequire = createRequire(cliEntry);
103
- const tools = {
104
- '@volter/world': packageVersion(cliEntry, '@volter/world'),
105
- '@volter/world-core': packageVersion(cliRequire.resolve('@volter/world-core/runtime'), '@volter/world-core'),
106
- '@volter/world-runtime': packageVersion(cliRequire.resolve('@volter/world-runtime'), '@volter/world-runtime'),
107
- };
120
+ const tools = consumerTools(cliEntry);
108
121
  const metadata = JSON.parse(readFileSync(join(packDir, 'package.json'), 'utf8'));
122
+ const appRoot = realpathSync(options.appDir), appRequire = createRequire(join(appRoot, 'package.json'));
123
+ const candidate = packageIdentity(appRequire.resolve(metadata.name), metadata.name);
124
+ const kernel = packageIdentity(appRequire.resolve('@volter/world-core/runtime'), '@volter/world-core');
125
+ for (const [name, identity] of [[metadata.name, candidate], ['@volter/world-core', kernel]]) {
126
+ const location = relative(join(appRoot, 'node_modules'), identity.directory);
127
+ if (location.startsWith('..') || isAbsolute(location))
128
+ throw new Error(`${name}: consumer resolves an evaluator or ancestor dependency; install it in the app`);
129
+ }
130
+ const appLockBytes = readFileSync(join(appRoot, 'package-lock.json'));
131
+ const appLock = JSON.parse(appLockBytes.toString());
132
+ if (candidate.version !== metadata.version || appLock.packages?.[`node_modules/${metadata.name}`]?.integrity !== options.artifactIntegrity)
133
+ throw new Error('Consumer app candidate does not match the exact assessed artifact');
134
+ if (kernel.version !== tools['@volter/world-core'] || appLock.packages?.['node_modules/@volter/world-core']?.version !== kernel.version)
135
+ throw new Error('Consumer app kernel does not match the CLI kernel pin');
136
+ 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 };
109
137
  const facts = JSON.parse(readFileSync(join(packDir, 'generated/pack-facts.json'), 'utf8'));
110
138
  const vendor = Object.keys(facts.packs ?? {})[0];
111
139
  if (!vendor || Object.keys(facts.packs).length !== 1)
@@ -186,7 +214,7 @@ export async function runConsumerWorkflow(packDir, options) {
186
214
  measured: true, passed: failures.length === 0 && teardownVerified,
187
215
  scope: 'Exact installed artifact; normal CLI initialization, unchanged SDK, result assertions and retained-state read-only resume where applicable',
188
216
  package: metadata.name, version: metadata.version, integrity: options.artifactIntegrity,
189
- tools,
217
+ tools, application,
190
218
  sourceInputsSha256: consumerSourceDigest(packDir), recipeSha256: createHash('sha256').update(JSON.stringify(recipe)).digest('hex'),
191
219
  fixture: existsSync(join(packDir, 'journeys/first-use.json')) ? 'journeys/first-use.json' : `standard/clients/${vendor}.first-use.json`, appDir: resolve(options.appDir), appName: basename(options.appDir),
192
220
  commands, failures, teardownVerified, retainedReadback,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/twin-standard",
3
- "version": "1.0.103",
3
+ "version": "1.0.104",
4
4
  "description": "The Protocol 3 standard for Volter twin packs: the criteria a pack is graded by, the grader, the gate a pack repository and the twin catalog run on every change and submission, and the spec derivation the grade checks against. Pin a version: every place a pack is judged judges by that standard.",
5
5
  "keywords": [
6
6
  "twin",
@@ -75,7 +75,7 @@
75
75
  "yaml": "2.9.0"
76
76
  },
77
77
  "peerDependencies": {
78
- "@volter/world-core": "3.0.111",
78
+ "@volter/world-core": "3.0.112",
79
79
  "@volter/world-runtime": "*",
80
80
  "playwright": "^1.63.0"
81
81
  },
package/src/cli.ts CHANGED
@@ -24,7 +24,7 @@ import { gatePublished } from './published.ts';
24
24
 
25
25
  const [command, ...args] = process.argv.slice(2);
26
26
  const usage = (): never => {
27
- console.error('usage: twin-standard gate <repo> [units…] | gate-published <name>@<version>… [--registry <url>] | grade <unit>… [--static] [--form] | derive <unit> [--out <dir>] | read-page <url> [--raw] | check-sources <unit> [--recheck] | conform <pack dir> | <name>@<version> [--registry <url>] | create <dir> --init <vendor> --package <@scope/name> | create <unit> --index | facts <pack dir> [--out <file>] | prepare-consumer <pack dir> --app <new dir> --cli <volter executable> --integrity <sha512> --out <json> | assess <pack dir> --prepared-consumer <json> [--browser] [--out <file>] | --version');
27
+ console.error('usage: twin-standard gate <repo> [units…] | gate-published <name>@<version>… [--registry <url>] | grade <unit>… [--static] [--form] | derive <unit> [--out <dir>] | read-page <url> [--raw] | check-sources <unit> [--recheck] | conform <pack dir> | <name>@<version> [--registry <url>] | create <dir> --init <vendor> --package <@scope/name> | create <unit> --index | facts <pack dir> [--out <file>] | prepare-consumer <pack dir> --app <new dir> --cli <volter executable> --integrity <sha512> --out <json> [--candidate <tgz>] | assess <pack dir> --prepared-consumer <json> [--browser] [--out <file>] | --version');
28
28
  process.exit(2);
29
29
  };
30
30
 
@@ -83,10 +83,12 @@ if (command === '--version') {
83
83
  const out = resolve(value('--out'));
84
84
  if (existsSync(out)) throw new Error('Prepared consumer output already exists');
85
85
  const { prepareConsumerApp } = await import('./consumer.ts');
86
- const fixture = prepareConsumerApp(resolve(args[0]!), appDir);
86
+ const candidateAt = args.indexOf('--candidate');
87
+ const artifact = candidateAt >= 0 ? resolve(value('--candidate')) : undefined;
88
+ const fixture = prepareConsumerApp(resolve(args[0]!), appDir, { cli, artifact });
87
89
  const { writeFileSync } = await import('node:fs');
88
90
  writeFileSync(out, JSON.stringify({ appDir, cli, artifactIntegrity }, null, 2) + '\n');
89
- console.log(JSON.stringify({ appDir, fixture, preparedConsumer: out, next: 'Install the app SDK dependencies with scripts disabled before assessing inside a World' }));
91
+ console.log(JSON.stringify({ appDir, fixture, preparedConsumer: out, next: 'Install the app SDK, candidate and kernel dependencies with scripts disabled before assessing inside a World' }));
90
92
  } else if (command === 'assess') {
91
93
  if (!args[0]) usage();
92
94
  const { assessPack } = await import('./assessment.ts');
package/src/consumer.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createHash } from 'node:crypto';
3
3
  import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
4
4
  import { createRequire } from 'node:module';
5
- import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
5
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
6
6
  import { spawn } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { standardPackageFile } from './package-file.ts';
@@ -17,6 +17,26 @@ export type ConsumerRecipe = {
17
17
  retention: { readback: string[] } | { none: string };
18
18
  };
19
19
  export type ConsumerOptions = { appDir: string; cli: string; artifactIntegrity: string };
20
+ type ConsumerInstallation = { cli: string; artifact?: string };
21
+
22
+ function packageIdentity(entry: string, name: string): { directory: string; version: string } {
23
+ let directory = dirname(realpathSync(entry));
24
+ while (true) {
25
+ const file = join(directory, 'package.json');
26
+ if (existsSync(file)) { const pkg = JSON.parse(readFileSync(file, 'utf8')); if (pkg.name === name) return { directory, version: pkg.version }; }
27
+ const parent = dirname(directory);
28
+ if (parent === directory) throw new Error(`${name} has no installed package identity`);
29
+ directory = parent;
30
+ }
31
+ }
32
+ function consumerTools(cli: string) {
33
+ const entry = realpathSync(cli), require = createRequire(entry);
34
+ return {
35
+ '@volter/world': packageIdentity(entry, '@volter/world').version,
36
+ '@volter/world-core': packageIdentity(require.resolve('@volter/world-core/runtime'), '@volter/world-core').version,
37
+ '@volter/world-runtime': packageIdentity(require.resolve('@volter/world-runtime'), '@volter/world-runtime').version,
38
+ };
39
+ }
20
40
 
21
41
  /** Bind behavior inputs on both source and packed layouts; compiled outputs and score caches are not inputs. */
22
42
  export function consumerSourceDigest(pack: string): string {
@@ -52,7 +72,7 @@ export function consumerRecipe(vendor: string, packDir?: string): ConsumerRecipe
52
72
  }
53
73
 
54
74
  /** Materialize only a new caller-owned app. Normal dependency installation happens before offline assessment. */
55
- export function prepareConsumerApp(packDir: string, appDir: string): { recipeSha256: string; dependencies: Record<string, string> } {
75
+ export function prepareConsumerApp(packDir: string, appDir: string, installation?: ConsumerInstallation): { recipeSha256: string; dependencies: Record<string, string> } {
56
76
  const facts = JSON.parse(readFileSync(join(packDir, 'generated/pack-facts.json'), 'utf8'));
57
77
  const vendor = Object.keys(facts.packs ?? {})[0];
58
78
  if (!vendor || Object.keys(facts.packs).length !== 1) throw new Error('Consumer preparation requires one artifact vendor identity');
@@ -66,8 +86,16 @@ export function prepareConsumerApp(packDir: string, appDir: string): { recipeSha
66
86
  const files = Object.entries(recipe.files).map(([path, content]) => {
67
87
  if (isAbsolute(path) || path.includes('\\') || path.split('/').some((part) => !part || part === '.' || part === '..') || path.split('/').includes('node_modules') || ['.env', '.npmrc', '.volter/world.json'].includes(path)) throw new Error(`Invalid consumer file: ${path}`);
68
88
  if (typeof content !== 'string') throw new Error('Consumer files must contain text');
69
- return [path, content] as const;
89
+ return [path, content] as [string, string];
70
90
  });
91
+ if (installation) {
92
+ const metadata = JSON.parse(readFileSync(join(packDir, 'package.json'), 'utf8'));
93
+ const tools = consumerTools(installation.cli);
94
+ if (installation.artifact && (!isAbsolute(installation.artifact) || !statSync(installation.artifact).isFile())) throw new Error('Consumer candidate must be an absolute prepared archive path');
95
+ if (Object.hasOwn(app.dependencies ?? {}, metadata.name)) throw new Error('Consumer SDK dependencies must not substitute the candidate package');
96
+ app.devDependencies = { ...app.devDependencies, '@volter/world-core': tools['@volter/world-core'], [metadata.name]: installation.artifact ? `file:${installation.artifact}` : metadata.version };
97
+ files.find(([path]) => path === 'package.json')![1] = JSON.stringify(app, null, 2) + '\n';
98
+ }
71
99
  mkdirSync(appDir, { recursive: true });
72
100
  for (const [path, content] of files) {
73
101
  mkdirSync(dirname(join(appDir, path)), { recursive: true });
@@ -79,24 +107,21 @@ export function prepareConsumerApp(packDir: string, appDir: string): { recipeSha
79
107
  export async function runConsumerWorkflow(packDir: string, options: ConsumerOptions) {
80
108
  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');
81
109
  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');
82
- const packageVersion = (entry: string, name: string): string => {
83
- let directory = dirname(entry);
84
- while (true) {
85
- const file = join(directory, 'package.json');
86
- if (existsSync(file)) { const pkg = JSON.parse(readFileSync(file, 'utf8')); if (pkg.name === name) return pkg.version; }
87
- const parent = dirname(directory);
88
- if (parent === directory) throw new Error(`${name} has no installed package identity`);
89
- directory = parent;
90
- }
91
- };
92
110
  const cliEntry = realpathSync(options.cli);
93
- const cliRequire = createRequire(cliEntry);
94
- const tools = {
95
- '@volter/world': packageVersion(cliEntry, '@volter/world'),
96
- '@volter/world-core': packageVersion(cliRequire.resolve('@volter/world-core/runtime'), '@volter/world-core'),
97
- '@volter/world-runtime': packageVersion(cliRequire.resolve('@volter/world-runtime'), '@volter/world-runtime'),
98
- };
111
+ const tools = consumerTools(cliEntry);
99
112
  const metadata = JSON.parse(readFileSync(join(packDir, 'package.json'), 'utf8'));
113
+ const appRoot = realpathSync(options.appDir), appRequire = createRequire(join(appRoot, 'package.json'));
114
+ const candidate = packageIdentity(appRequire.resolve(metadata.name), metadata.name);
115
+ const kernel = packageIdentity(appRequire.resolve('@volter/world-core/runtime'), '@volter/world-core');
116
+ for (const [name, identity] of [[metadata.name, candidate], ['@volter/world-core', kernel]] as const) {
117
+ const location = relative(join(appRoot, 'node_modules'), identity.directory);
118
+ if (location.startsWith('..') || isAbsolute(location)) throw new Error(`${name}: consumer resolves an evaluator or ancestor dependency; install it in the app`);
119
+ }
120
+ const appLockBytes = readFileSync(join(appRoot, 'package-lock.json'));
121
+ const appLock = JSON.parse(appLockBytes.toString());
122
+ if (candidate.version !== metadata.version || appLock.packages?.[`node_modules/${metadata.name}`]?.integrity !== options.artifactIntegrity) throw new Error('Consumer app candidate does not match the exact assessed artifact');
123
+ if (kernel.version !== tools['@volter/world-core'] || appLock.packages?.['node_modules/@volter/world-core']?.version !== kernel.version) throw new Error('Consumer app kernel does not match the CLI kernel pin');
124
+ 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 };
100
125
  const facts = JSON.parse(readFileSync(join(packDir, 'generated/pack-facts.json'), 'utf8'));
101
126
  const vendor = Object.keys(facts.packs ?? {})[0];
102
127
  if (!vendor || Object.keys(facts.packs).length !== 1) throw new Error('Consumer workflow requires one selected artifact identity');
@@ -163,7 +188,7 @@ export async function runConsumerWorkflow(packDir: string, options: ConsumerOpti
163
188
  measured: true, passed: failures.length === 0 && teardownVerified,
164
189
  scope: 'Exact installed artifact; normal CLI initialization, unchanged SDK, result assertions and retained-state read-only resume where applicable',
165
190
  package: metadata.name, version: metadata.version, integrity: options.artifactIntegrity,
166
- tools,
191
+ tools, application,
167
192
  sourceInputsSha256: consumerSourceDigest(packDir), recipeSha256: createHash('sha256').update(JSON.stringify(recipe)).digest('hex'),
168
193
  fixture: existsSync(join(packDir, 'journeys/first-use.json')) ? 'journeys/first-use.json' : `standard/clients/${vendor}.first-use.json`, appDir: resolve(options.appDir), appName: basename(options.appDir),
169
194
  commands, failures, teardownVerified, retainedReadback,