@volter/twin-standard 1.0.102 → 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 +6 -2
- package/dist/src/assessment.d.ts +13 -0
- package/dist/src/cli.js +5 -3
- package/dist/src/consumer.d.ts +19 -1
- package/dist/src/consumer.js +52 -24
- package/package.json +2 -2
- package/src/cli.ts +5 -3
- package/src/consumer.ts +45 -20
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
|
-
|
|
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
|
package/dist/src/assessment.d.ts
CHANGED
|
@@ -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
|
|
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])
|
package/dist/src/consumer.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/src/consumer.js
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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,
|