@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.
- package/README.md +3 -2
- package/client-sdks/package.json +1 -0
- package/client-sdks/pnpm-lock.yaml +210 -0
- package/dist/client-sdks/package.json +1 -0
- package/dist/client-sdks/pnpm-lock.yaml +210 -0
- package/dist/src/assessment.d.ts +13 -3
- package/dist/src/assessment.js +10 -6
- package/dist/src/browser-assessment.js +16 -13
- package/dist/src/clients/harness.d.ts +25 -4
- package/dist/src/clients/harness.js +184 -22
- package/dist/src/clients/jira.js +11 -2
- package/dist/src/clients/run.js +19 -14
- package/dist/src/conformance.d.ts +2 -0
- package/dist/src/conformance.js +392 -184
- package/dist/src/consumer.d.ts +22 -1
- package/dist/src/consumer.js +91 -26
- package/dist/src/credential-issuer-import.d.ts +3 -0
- package/dist/src/credential-issuer-import.js +42 -0
- package/dist/src/derive.js +3 -0
- package/dist/src/gate.js +4 -0
- package/dist/src/lanes.d.ts +2 -0
- package/dist/src/lanes.js +7 -0
- package/dist/src/protocol-3.js +8 -3
- package/dist/src/spec-documents.d.ts +4 -0
- package/dist/src/spec-documents.js +110 -3
- package/dist/src/tools/life-coverage-walk.js +38 -26
- package/dist/src/tools/score-pack.js +6 -5
- package/dist/src/tools/vendor-examples.js +91 -80
- package/dist/src/walk/behavior-journey.d.ts +22 -14
- package/dist/src/walk/behavior-journey.js +182 -133
- package/dist/src/walk/journey-kit.d.ts +3 -2
- package/dist/src/walk/journey-kit.js +6 -2
- package/dist/src/walk/replay.d.ts +23 -0
- package/dist/src/walk/replay.js +118 -0
- package/dist/src/walk/serve-pack.d.ts +2 -2
- package/dist/src/walk/serve-pack.js +38 -2
- package/dist/src/walk/shape.d.ts +25 -4
- package/dist/src/walk/shape.js +166 -56
- package/dist/src/walk/vendor-fetch.d.ts +7 -2
- package/dist/src/walk/vendor-fetch.js +13 -2
- package/dist/src/walk/walk-database.d.ts +9 -1
- package/dist/src/walk/walk-database.js +35 -13
- package/dist/src/walk/xml-view.d.ts +3 -2
- package/dist/src/walk/xml-view.js +5 -4
- package/package.json +3 -3
- package/src/assessment.ts +12 -8
- package/src/browser-assessment.ts +12 -5
- package/src/clients/harness.ts +142 -19
- package/src/clients/jira.ts +4 -2
- package/src/clients/run.ts +17 -15
- package/src/conformance.ts +313 -128
- package/src/consumer.ts +83 -21
- package/src/credential-issuer-import.ts +38 -0
- package/src/derive.ts +3 -1
- package/src/gate.ts +3 -0
- package/src/lanes.ts +7 -0
- package/src/protocol-3.ts +7 -3
- package/src/spec-documents.ts +92 -3
- package/src/tools/life-coverage-walk.ts +32 -22
- package/src/tools/score-pack.ts +7 -6
- package/src/tools/vendor-examples.ts +88 -75
- package/src/walk/behavior-journey.ts +145 -109
- package/src/walk/journey-kit.ts +7 -5
- package/src/walk/replay.ts +103 -0
- package/src/walk/serve-pack.ts +25 -5
- package/src/walk/shape.ts +156 -54
- package/src/walk/vendor-fetch.ts +16 -4
- package/src/walk/walk-database.ts +41 -15
- package/src/walk/xml-view.ts +5 -4
package/dist/src/consumer.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ type ConsumerInstallation = {
|
|
|
23
23
|
cli: string;
|
|
24
24
|
artifact?: string;
|
|
25
25
|
};
|
|
26
|
+
export type ConsumerResult = Awaited<ReturnType<typeof runConsumerWorkflow>>;
|
|
26
27
|
/** Bind behavior inputs on both source and packed layouts; compiled outputs and score caches are not inputs. */
|
|
27
28
|
export declare function consumerSourceDigest(pack: string): string;
|
|
28
29
|
/** A pack supplies its own entry journey; older immutable releases use a versioned standard fixture. */
|
|
@@ -32,6 +33,12 @@ export declare function prepareConsumerApp(packDir: string, appDir: string, inst
|
|
|
32
33
|
recipeSha256: string;
|
|
33
34
|
dependencies: Record<string, string>;
|
|
34
35
|
};
|
|
36
|
+
/** Run the unchanged customer command, with the environment supplied by its World. */
|
|
37
|
+
export declare function runConsumerCommand(argv: string[], appDir: string, env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<{
|
|
38
|
+
exitCode: number | null;
|
|
39
|
+
stdout: string;
|
|
40
|
+
stderr: string;
|
|
41
|
+
}>;
|
|
35
42
|
export declare function runConsumerWorkflow(packDir: string, options: ConsumerOptions): Promise<{
|
|
36
43
|
node: string | null;
|
|
37
44
|
evaluatorRuntime: {
|
|
@@ -40,7 +47,7 @@ export declare function runConsumerWorkflow(packDir: string, options: ConsumerOp
|
|
|
40
47
|
};
|
|
41
48
|
at: string;
|
|
42
49
|
retentionNotApplicable?: string | undefined;
|
|
43
|
-
measured:
|
|
50
|
+
measured: true;
|
|
44
51
|
passed: boolean;
|
|
45
52
|
scope: string;
|
|
46
53
|
package: any;
|
|
@@ -61,6 +68,15 @@ export declare function runConsumerWorkflow(packDir: string, options: ConsumerOp
|
|
|
61
68
|
package: string;
|
|
62
69
|
version: string;
|
|
63
70
|
};
|
|
71
|
+
peers: {
|
|
72
|
+
package: string;
|
|
73
|
+
version: string;
|
|
74
|
+
}[];
|
|
75
|
+
sdks: {
|
|
76
|
+
package: string;
|
|
77
|
+
version: string;
|
|
78
|
+
entry: string;
|
|
79
|
+
}[];
|
|
64
80
|
dependencyLockSha256: string;
|
|
65
81
|
appLocal: boolean;
|
|
66
82
|
};
|
|
@@ -90,4 +106,9 @@ export declare function runConsumerWorkflow(packDir: string, options: ConsumerOp
|
|
|
90
106
|
mode: string;
|
|
91
107
|
};
|
|
92
108
|
}>;
|
|
109
|
+
/** The same installed execution can prove a declared SDK when the standard has no built-in client module. */
|
|
110
|
+
export declare function consumerClientProof(packDir: string, vendor: string, sdks: string[], result?: ConsumerResult): {
|
|
111
|
+
failures: string[];
|
|
112
|
+
note?: string;
|
|
113
|
+
};
|
|
93
114
|
export {};
|
package/dist/src/consumer.js
CHANGED
|
@@ -7,16 +7,27 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { standardPackageFile } from "./package-file.js";
|
|
9
9
|
import { dependencyLock } from "./dependency-lock.js";
|
|
10
|
+
function recipeDependencies(recipe) {
|
|
11
|
+
const app = JSON.parse(recipe.files['package.json']);
|
|
12
|
+
const dependencies = { ...app.dependencies, ...app.devDependencies, ...app.optionalDependencies };
|
|
13
|
+
for (const [name, version] of Object.entries(dependencies)) {
|
|
14
|
+
if (typeof version !== 'string' || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
|
|
15
|
+
throw new Error(`${name}: customer SDK dependencies require exact registry versions`);
|
|
16
|
+
}
|
|
17
|
+
return dependencies;
|
|
18
|
+
}
|
|
10
19
|
function packageIdentity(entry, name) {
|
|
11
20
|
let directory = dirname(realpathSync(entry));
|
|
12
21
|
while (true) {
|
|
13
|
-
const file = join(directory, 'package.json');
|
|
14
|
-
if (existsSync(file)) {
|
|
15
|
-
const pkg = JSON.parse(readFileSync(file, 'utf8'));
|
|
16
|
-
if (pkg.name === name)
|
|
17
|
-
return { directory, version: pkg.version };
|
|
18
|
-
}
|
|
19
22
|
const parent = dirname(directory);
|
|
23
|
+
// The installed node_modules slot owns the entry; nested distribution manifests do not identify the package.
|
|
24
|
+
const boundary = basename(parent) === 'node_modules' || (basename(parent).startsWith('@') && basename(dirname(parent)) === 'node_modules');
|
|
25
|
+
if (boundary) {
|
|
26
|
+
const pkg = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));
|
|
27
|
+
if (pkg.name !== name)
|
|
28
|
+
throw new Error(`${name}: installed package boundary belongs to ${String(pkg.name)}`);
|
|
29
|
+
return { directory, version: pkg.version };
|
|
30
|
+
}
|
|
20
31
|
if (parent === directory)
|
|
21
32
|
throw new Error(`${name} has no installed package identity`);
|
|
22
33
|
directory = parent;
|
|
@@ -30,6 +41,21 @@ function consumerTools(cli) {
|
|
|
30
41
|
'@volter/world-runtime': packageIdentity(require.resolve('@volter/world-runtime'), '@volter/world-runtime').version,
|
|
31
42
|
};
|
|
32
43
|
}
|
|
44
|
+
function consumerEntries(directory, names) {
|
|
45
|
+
if (!names.length)
|
|
46
|
+
return new Map();
|
|
47
|
+
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' });
|
|
48
|
+
if (result.error || result.status !== 0)
|
|
49
|
+
throw new Error(`Consumer ESM dependency resolution failed: ${result.error?.message ?? result.stderr.trim()}`);
|
|
50
|
+
const entries = JSON.parse(result.stdout);
|
|
51
|
+
return new Map(names.map((name, index) => [name, fileURLToPath(entries[index])]));
|
|
52
|
+
}
|
|
53
|
+
function consumerPeerVersions(metadata, cli, sdkDependencies) {
|
|
54
|
+
const names = Object.keys(metadata.peerDependencies ?? {})
|
|
55
|
+
.filter((name) => name !== '@volter/world-core' && !metadata.peerDependenciesMeta?.[name]?.optional && !metadata.dependencies?.[name] && !metadata.optionalDependencies?.[name] && !sdkDependencies[name]);
|
|
56
|
+
const entries = consumerEntries(dirname(realpathSync(cli)), names);
|
|
57
|
+
return Object.fromEntries(names.map((name) => [name, packageIdentity(entries.get(name), name).version]));
|
|
58
|
+
}
|
|
33
59
|
/** Bind behavior inputs on both source and packed layouts; compiled outputs and score caches are not inputs. */
|
|
34
60
|
export function consumerSourceDigest(pack) {
|
|
35
61
|
const entries = [];
|
|
@@ -86,10 +112,7 @@ export function prepareConsumerApp(packDir, appDir, installation) {
|
|
|
86
112
|
app.devEngines = { packageManager: { name: 'pnpm', version: '12.15.0', onFail: 'error' } };
|
|
87
113
|
if (app.scripts || app.workspaces)
|
|
88
114
|
throw new Error('Consumer fixture supplies direct SDK commands, not install scripts or workspace links');
|
|
89
|
-
|
|
90
|
-
if (typeof version !== 'string' || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
|
|
91
|
-
throw new Error(`${name}: customer SDK dependencies require exact registry versions`);
|
|
92
|
-
}
|
|
115
|
+
const sdkDependencies = recipeDependencies(recipe);
|
|
93
116
|
const files = Object.entries(recipe.files).map(([path, content]) => {
|
|
94
117
|
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))
|
|
95
118
|
throw new Error(`Invalid consumer file: ${path}`);
|
|
@@ -104,7 +127,7 @@ export function prepareConsumerApp(packDir, appDir, installation) {
|
|
|
104
127
|
throw new Error('Consumer candidate must be an absolute prepared archive path');
|
|
105
128
|
if (Object.hasOwn(app.dependencies ?? {}, metadata.name))
|
|
106
129
|
throw new Error('Consumer SDK dependencies must not substitute the candidate package');
|
|
107
|
-
app.devDependencies = { ...app.devDependencies, '@volter/world-core': tools['@volter/world-core'], [metadata.name]: installation.artifact ? `file:${installation.artifact}` : metadata.version };
|
|
130
|
+
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 };
|
|
108
131
|
}
|
|
109
132
|
files.find(([path]) => path === 'package.json')[1] = JSON.stringify(app, null, 2) + '\n';
|
|
110
133
|
files.push(['pnpm-workspace.yaml', 'packages:\n - .\npackageImportMethod: hardlink\nnodeLinker: hoisted\nautoInstallPeers: false\nstrictPeerDependencies: false\n']);
|
|
@@ -126,6 +149,17 @@ export function prepareConsumerApp(packDir, appDir, installation) {
|
|
|
126
149
|
}
|
|
127
150
|
return { recipeSha256: createHash('sha256').update(JSON.stringify(recipe)).digest('hex'), dependencies: app.dependencies ?? {} };
|
|
128
151
|
}
|
|
152
|
+
/** Run the unchanged customer command, with the environment supplied by its World. */
|
|
153
|
+
export function runConsumerCommand(argv, appDir, env, signal) {
|
|
154
|
+
return new Promise((done) => {
|
|
155
|
+
const child = spawn(argv[0], argv.slice(1), { cwd: appDir, env, signal, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
156
|
+
let stdout = '', stderr = '';
|
|
157
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
158
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
159
|
+
child.on('error', (error) => { stderr += `\n${error.message}`; });
|
|
160
|
+
child.on('close', (exitCode) => done({ exitCode, stdout, stderr }));
|
|
161
|
+
});
|
|
162
|
+
}
|
|
129
163
|
export async function runConsumerWorkflow(packDir, options) {
|
|
130
164
|
if (!isAbsolute(options.appDir) || !isAbsolute(options.cli) || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(options.artifactIntegrity))
|
|
131
165
|
throw new Error('Consumer requires absolute prepared paths and candidate SHA-512 integrity');
|
|
@@ -134,10 +168,18 @@ export async function runConsumerWorkflow(packDir, options) {
|
|
|
134
168
|
const cliEntry = realpathSync(options.cli);
|
|
135
169
|
const tools = consumerTools(cliEntry);
|
|
136
170
|
const metadata = JSON.parse(readFileSync(join(packDir, 'package.json'), 'utf8'));
|
|
171
|
+
const facts = JSON.parse(readFileSync(join(packDir, 'generated/pack-facts.json'), 'utf8'));
|
|
172
|
+
const vendor = Object.keys(facts.packs ?? {})[0];
|
|
173
|
+
if (!vendor || Object.keys(facts.packs).length !== 1)
|
|
174
|
+
throw new Error('Consumer workflow requires one selected artifact identity');
|
|
175
|
+
const recipe = consumerRecipe(vendor, packDir), sdkDependencies = recipeDependencies(recipe);
|
|
137
176
|
const appRoot = realpathSync(options.appDir), appRequire = createRequire(join(appRoot, 'package.json'));
|
|
138
177
|
const candidate = packageIdentity(appRequire.resolve(metadata.name), metadata.name);
|
|
139
178
|
const kernel = packageIdentity(appRequire.resolve('@volter/world-core/runtime'), '@volter/world-core');
|
|
140
|
-
|
|
179
|
+
const peerVersions = consumerPeerVersions(metadata, cliEntry, sdkDependencies);
|
|
180
|
+
const entries = consumerEntries(appRoot, [...Object.keys(peerVersions), ...Object.keys(sdkDependencies)]);
|
|
181
|
+
const peers = Object.keys(peerVersions).map((name) => [name, packageIdentity(entries.get(name), name)]);
|
|
182
|
+
for (const [name, identity] of [[metadata.name, candidate], ['@volter/world-core', kernel], ...peers]) {
|
|
141
183
|
const location = relative(join(appRoot, 'node_modules'), identity.directory);
|
|
142
184
|
if (location.startsWith('..') || isAbsolute(location))
|
|
143
185
|
throw new Error(`${name}: consumer resolves an evaluator or ancestor dependency; install it in the app`);
|
|
@@ -147,25 +189,25 @@ export async function runConsumerWorkflow(packDir, options) {
|
|
|
147
189
|
throw new Error('Consumer app candidate does not match the exact assessed artifact');
|
|
148
190
|
if (kernel.version !== tools['@volter/world-core'] || appLock.entry('@volter/world-core')?.version !== kernel.version)
|
|
149
191
|
throw new Error('Consumer app kernel does not match the CLI kernel pin');
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
192
|
+
for (const [name, identity] of peers)
|
|
193
|
+
if (identity.version !== peerVersions[name] || appLock.entry(name)?.version !== identity.version)
|
|
194
|
+
throw new Error(`${name}: consumer peer does not match the installed CLI closure and exact application lock version ${peerVersions[name]}`);
|
|
195
|
+
const sdks = Object.entries(sdkDependencies).map(([name, version]) => {
|
|
196
|
+
const entry = realpathSync(entries.get(name)), identity = packageIdentity(entry, name);
|
|
197
|
+
const location = relative(join(appRoot, 'node_modules'), identity.directory);
|
|
198
|
+
if (location.startsWith('..') || isAbsolute(location))
|
|
199
|
+
throw new Error(`${name}: customer SDK resolves outside the application installation`);
|
|
200
|
+
if (identity.version !== version || appLock.entry(name)?.version !== version)
|
|
201
|
+
throw new Error(`${name}: customer SDK does not match its exact recipe and application lock version ${version}`);
|
|
202
|
+
return { package: name, version, entry: relative(appRoot, entry).split('\\').join('/') };
|
|
203
|
+
});
|
|
204
|
+
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 };
|
|
156
205
|
const commands = [];
|
|
157
206
|
const failures = [];
|
|
158
207
|
const compiledCli = /\.[cm]?js$/.test(cliEntry);
|
|
159
208
|
let nodeVersion = null;
|
|
160
209
|
const execute = async (label, executable, argv) => {
|
|
161
|
-
const result = await
|
|
162
|
-
const child = spawn(executable, argv, { cwd: options.appDir, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
163
|
-
let stdout = '', stderr = '';
|
|
164
|
-
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
165
|
-
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
166
|
-
child.on('error', (error) => done({ exitCode: 1, stdout, stderr: `${stderr}\n${error.message}` }));
|
|
167
|
-
child.on('close', (exitCode) => done({ exitCode, stdout, stderr }));
|
|
168
|
-
});
|
|
210
|
+
const result = await runConsumerCommand([executable, ...argv], options.appDir, process.env);
|
|
169
211
|
commands.push({ label, argv: [executable, ...argv], ...result });
|
|
170
212
|
if (result.exitCode !== 0)
|
|
171
213
|
throw new Error(`${label} exited ${result.exitCode}: ${result.stderr.trim().split('\n')[0] ?? ''}`);
|
|
@@ -256,3 +298,26 @@ export async function runConsumerWorkflow(packDir, options) {
|
|
|
256
298
|
at: new Date().toISOString(),
|
|
257
299
|
};
|
|
258
300
|
}
|
|
301
|
+
/** The same installed execution can prove a declared SDK when the standard has no built-in client module. */
|
|
302
|
+
export function consumerClientProof(packDir, vendor, sdks, result) {
|
|
303
|
+
if (!existsSync(join(packDir, 'journeys/first-use.json')))
|
|
304
|
+
return { failures: ['the pack supplies no journeys/first-use.json installed SDK workflow'] };
|
|
305
|
+
const recipe = consumerRecipe(vendor, packDir);
|
|
306
|
+
const dependencies = Object.entries(recipeDependencies(recipe)).filter(([name]) => sdks.includes(name));
|
|
307
|
+
if (!dependencies.length)
|
|
308
|
+
return { failures: ['the installed workflow has no exact direct dependency named by adoption.sdks'] };
|
|
309
|
+
if (!result?.measured || !result.passed || result.failures.length || !result.teardownVerified)
|
|
310
|
+
return { failures: ['the installed SDK workflow has no passing execution with verified teardown'] };
|
|
311
|
+
const metadata = JSON.parse(readFileSync(join(packDir, 'package.json'), 'utf8'));
|
|
312
|
+
const candidate = result.application.candidate;
|
|
313
|
+
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)
|
|
314
|
+
return { failures: ['the installed SDK result does not identify this exact app-local candidate artifact'] };
|
|
315
|
+
if (result.fixture !== 'journeys/first-use.json' || result.sourceInputsSha256 !== consumerSourceDigest(packDir) || result.recipeSha256 !== createHash('sha256').update(JSON.stringify(recipe)).digest('hex'))
|
|
316
|
+
return { failures: ['the installed SDK result does not describe the current behavior inputs and authored recipe'] };
|
|
317
|
+
for (const [name, version] of dependencies) {
|
|
318
|
+
const sdk = result.application.sdks.find((entry) => entry.package === name);
|
|
319
|
+
if (sdk?.version !== version || !sdk.entry.startsWith('node_modules/') || sdk.entry.split('/').includes('..'))
|
|
320
|
+
return { failures: [`${name}: the installed SDK result has no exact app-local ${version} resolution`] };
|
|
321
|
+
}
|
|
322
|
+
return { failures: [], note: `installed customer workflow through ${dependencies.map(([name, version]) => `${name}@${version}`).join(', ')}; exact artifact and current recipe` };
|
|
323
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** The declared credential issuer's public subpaths, pinned as a direct runtime dependency. Reading the manifest as
|
|
2
|
+
* data keeps the source/tarball gate independent of dependency installation; typechecking verifies exported paths. */
|
|
3
|
+
export declare function credentialIssuerImports(unitDir: string, packageDir: string): (specifier: string) => boolean;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
/** A literal property's effective value; a later spread or computed key cannot establish an import permission. */
|
|
5
|
+
function property(expression, name) {
|
|
6
|
+
while (expression && (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression)))
|
|
7
|
+
expression = expression.expression;
|
|
8
|
+
if (!expression || !ts.isObjectLiteralExpression(expression))
|
|
9
|
+
return undefined;
|
|
10
|
+
for (const member of [...expression.properties].reverse()) {
|
|
11
|
+
if (!ts.isPropertyAssignment(member) || ts.isComputedPropertyName(member.name))
|
|
12
|
+
return undefined;
|
|
13
|
+
if (member.name.text === name)
|
|
14
|
+
return member.initializer;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
/** The declared credential issuer's public subpaths, pinned as a direct runtime dependency. Reading the manifest as
|
|
19
|
+
* data keeps the source/tarball gate independent of dependency installation; typechecking verifies exported paths. */
|
|
20
|
+
export function credentialIssuerImports(unitDir, packageDir) {
|
|
21
|
+
const path = join(unitDir, 'src', 'manifest.ts');
|
|
22
|
+
const packagePath = join(packageDir, 'package.json');
|
|
23
|
+
if (!existsSync(path) || !existsSync(packagePath))
|
|
24
|
+
return () => false;
|
|
25
|
+
const source = ts.createSourceFile(path, readFileSync(path, 'utf8'), ts.ScriptTarget.Latest, true);
|
|
26
|
+
const manifests = source.statements.flatMap(statement => ts.isVariableStatement(statement)
|
|
27
|
+
&& statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
|
|
28
|
+
? statement.declarationList.declarations.filter(declaration => ts.isIdentifier(declaration.name) && declaration.name.text === 'manifest') : []);
|
|
29
|
+
const issuer = manifests.length === 1 ? property(property(manifests[0].initializer, 'descriptor'), 'credentialIssuer') : undefined;
|
|
30
|
+
if (!issuer || !ts.isStringLiteral(issuer) || !/^[a-z]/.test(issuer.text) || /[^a-z0-9-]/.test(issuer.text))
|
|
31
|
+
return () => false;
|
|
32
|
+
const packageName = `@volter/twin-${issuer.text}`;
|
|
33
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
34
|
+
const version = pkg.dependencies?.[packageName];
|
|
35
|
+
const number = '(?:0|[1-9][0-9]*)';
|
|
36
|
+
const prerelease = '(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)';
|
|
37
|
+
const exactVersion = new RegExp(`^${number}\\.${number}\\.${number}(?:-${prerelease}(?:\\.${prerelease})*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$`);
|
|
38
|
+
if (pkg.name === packageName || typeof version !== 'string' || exactVersion.exec(version)?.[0] !== version)
|
|
39
|
+
return () => false;
|
|
40
|
+
const prefix = `${packageName}/`;
|
|
41
|
+
return specifier => specifier.startsWith(prefix) && specifier.slice(prefix.length).split('/').every(segment => segment !== '' && segment !== '.' && segment !== '..' && !/[^A-Za-z0-9._-]/.test(segment) && segment.toLowerCase() !== 'node_modules');
|
|
42
|
+
}
|
package/dist/src/derive.js
CHANGED
|
@@ -415,6 +415,9 @@ if (existsSync(manifestPath)) {
|
|
|
415
415
|
const o = op(r.get);
|
|
416
416
|
if (o?.class === 'retrieve' && ours(o))
|
|
417
417
|
return [];
|
|
418
|
+
// A known subject may be addressed by an id query whose native answer is a list envelope.
|
|
419
|
+
if (r.known && o?.class === 'list' && o.answers?.list && ours(o) && o.method.toUpperCase() === 'GET')
|
|
420
|
+
return [];
|
|
418
421
|
// a resource the spec does not name (a client-derived spec classes every request an action) is read by the GET
|
|
419
422
|
// the scope names, the answer itself the subject
|
|
420
423
|
if (o && !specNamed.has(name) && !o.resource && String(o.method).toUpperCase() === 'GET')
|
package/dist/src/gate.js
CHANGED
|
@@ -19,6 +19,7 @@ import ts from 'typescript';
|
|
|
19
19
|
import { grade } from "./grade.js";
|
|
20
20
|
import { withCheckoutFiles } from "./protocol-3.js";
|
|
21
21
|
import { isPackArtifact } from "./pack-files.js";
|
|
22
|
+
import { credentialIssuerImports } from "./credential-issuer-import.js";
|
|
22
23
|
/** Every unit of a pack repository: each pack (a directory with a package.json) and each lane in it (a directory with
|
|
23
24
|
* its own src/manifest.ts and no package.json). */
|
|
24
25
|
export function unitsOf(repo) {
|
|
@@ -115,6 +116,7 @@ export function laneRootFailures(dir) {
|
|
|
115
116
|
failures.push('layout: src/fetch.ts builds no createVendorFetch: the kernel routes a vendor\'s lanes from its manifest, nothing by hand');
|
|
116
117
|
// the root's own files hold to the pack contract as a unit's do (the grade's kernel-contract)
|
|
117
118
|
const beyond = [];
|
|
119
|
+
const issuerImport = credentialIssuerImports(dir, dir);
|
|
118
120
|
for (const n of existsSync(join(dir, 'src')) ? readdirSync(join(dir, 'src')).filter((x) => /\.tsx?$/.test(x) && !x.endsWith('.d.ts')) : []) {
|
|
119
121
|
// the file's imports as TypeScript reads them, as the grade's kernel-contract does
|
|
120
122
|
for (const from of ts.preProcessFile(readFileSync(join(dir, 'src', n), 'utf8'), true, true).importedFiles.map((i) => i.fileName)) {
|
|
@@ -131,6 +133,8 @@ export function laneRootFailures(dir) {
|
|
|
131
133
|
}
|
|
132
134
|
if (from.startsWith('.') || from === '@volter/world-core' || from === '@volter/world-ui')
|
|
133
135
|
continue;
|
|
136
|
+
if (issuerImport(from))
|
|
137
|
+
continue;
|
|
134
138
|
if (n === 'cli.ts' && (from === '@volter/world-core/args' || from === '@volter/world-core/lifecycle'))
|
|
135
139
|
continue;
|
|
136
140
|
beyond.push(`src/${n} imports ${from}`);
|
package/dist/src/lanes.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** The pack and its manifest-bearing lanes, without crossing another package's boundary. */
|
|
2
|
+
export declare function unitsIn(pack: string): string[];
|
|
1
3
|
/** Metadata such as sources.json is not a derivable API of a vendor whose APIs all belong to its lanes. */
|
|
2
4
|
export declare function hasOwnSurface(pkg: string): boolean;
|
|
3
5
|
/** The lanes over a vendor's state in the vendor's directory `pkg`, wherever its repository is. */
|
package/dist/src/lanes.js
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
// walked, measured and graded through the vendor's life; every other lane keeps its own.
|
|
4
4
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
/** The pack and its manifest-bearing lanes, without crossing another package's boundary. */
|
|
7
|
+
export function unitsIn(pack) {
|
|
8
|
+
if (!existsSync(pack))
|
|
9
|
+
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
|
+
}
|
|
6
13
|
/** Metadata such as sources.json is not a derivable API of a vendor whose APIs all belong to its lanes. */
|
|
7
14
|
export function hasOwnSurface(pkg) {
|
|
8
15
|
if (existsSync(join(pkg, 'src', 'generated', 'surface.gen.json')))
|
package/dist/src/protocol-3.js
CHANGED
|
@@ -25,6 +25,7 @@ import ts from 'typescript';
|
|
|
25
25
|
import { fileURLToPath } from 'node:url';
|
|
26
26
|
import { sharingLanesIn } from "./lanes.js";
|
|
27
27
|
import { isPackArtifact } from "./pack-files.js";
|
|
28
|
+
import { credentialIssuerImports } from "./credential-issuer-import.js";
|
|
28
29
|
// A path relative to another with '/' between its parts on every platform: git lists files that way, and every rule
|
|
29
30
|
// here compares and splits on '/'; Windows' relative() joins with '\', which hid every lane's files and spec.
|
|
30
31
|
const rel = (from, to) => relative(from, to).split(sep).join('/');
|
|
@@ -387,7 +388,7 @@ const criteria = [
|
|
|
387
388
|
},
|
|
388
389
|
{
|
|
389
390
|
id: 'handler-contract', section: 'form',
|
|
390
|
-
asks: 'every file in semantics/ reaches no randomness, clock, environment, network or module but the kernel, the states
|
|
391
|
+
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',
|
|
391
392
|
check(unit) {
|
|
392
393
|
const dir = join(unit.dir, 'src', 'semantics');
|
|
393
394
|
if (!existsSync(dir))
|
|
@@ -395,6 +396,7 @@ const criteria = [
|
|
|
395
396
|
const ops = operationsOf(unit);
|
|
396
397
|
const families = new Set(ops.values());
|
|
397
398
|
const found = [];
|
|
399
|
+
const issuerImport = credentialIssuerImports(unit.dir, packageOf(unit));
|
|
398
400
|
for (const f of filesUnder(dir).filter((x) => /\.tsx?$/.test(x) && !TEST_FILE.test(x))) {
|
|
399
401
|
const text = stripComments(readFileSync(join(dir, f), 'utf8'));
|
|
400
402
|
const raw = readFileSync(join(dir, f), 'utf8');
|
|
@@ -413,7 +415,7 @@ const criteria = [
|
|
|
413
415
|
const engineModule = /^\.\.\/engine\/[A-Za-z0-9_-]+\.ts$/.test(from) || vendorPath(unit, from, 3, /^engine\/[A-Za-z0-9_-]+\.ts$/);
|
|
414
416
|
// a lane over its vendor's state takes the vendor's states and shared helpers ("Pack layout")
|
|
415
417
|
const vendorShared = vendorPath(unit, from, 3, /^(semantics\/(states|shared)|manifest)\.ts$/);
|
|
416
|
-
if (from !== '@volter/world-core' && !/^\.\/(states|shared)\.ts$/.test(from) && !mapImport && !engineModule && !vendorShared)
|
|
418
|
+
if (from !== '@volter/world-core' && !/^\.\/(states|shared)\.ts$/.test(from) && !mapImport && !engineModule && !vendorShared && !issuerImport(from))
|
|
417
419
|
found.push(`${f} imports ${from}`);
|
|
418
420
|
}
|
|
419
421
|
// the handler map is typed over the narrow context, so the compiler holds every handler it names to the contract
|
|
@@ -546,12 +548,13 @@ const criteria = [
|
|
|
546
548
|
},
|
|
547
549
|
{
|
|
548
550
|
id: 'kernel-contract', section: 'form',
|
|
549
|
-
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",
|
|
551
|
+
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",
|
|
550
552
|
check(unit) {
|
|
551
553
|
const src = join(unit.dir, 'src');
|
|
552
554
|
if (!existsSync(src))
|
|
553
555
|
return fail('src/ is missing');
|
|
554
556
|
const found = [];
|
|
557
|
+
const issuerImport = credentialIssuerImports(unit.dir, packageOf(unit));
|
|
555
558
|
for (const f of filesUnder(src).filter((x) => /\.tsx?$/.test(x) && !TEST_FILE.test(x) && !x.startsWith('generated/'))) {
|
|
556
559
|
// the file's imports, re-exports and dynamic imports as TypeScript reads them (a string that only reads like one,
|
|
557
560
|
// an error message's "from '<name>'", is none)
|
|
@@ -596,6 +599,8 @@ const criteria = [
|
|
|
596
599
|
continue;
|
|
597
600
|
if (from === '@volter/world-core' || from === '@volter/world-ui')
|
|
598
601
|
continue;
|
|
602
|
+
if (issuerImport(from))
|
|
603
|
+
continue;
|
|
599
604
|
if (f === 'cli.ts' && (from === '@volter/world-core/args' || from === '@volter/world-core/lifecycle'))
|
|
600
605
|
continue;
|
|
601
606
|
found.push(`${f} imports ${from}`);
|
|
@@ -4,6 +4,10 @@ export type SpecDocuments = {
|
|
|
4
4
|
};
|
|
5
5
|
/** The documents of `spec/openapi/`, by name, or undefined when the pack vendors one spec file. */
|
|
6
6
|
export declare function readSpecDocuments(specDir: string): SpecDocuments | undefined;
|
|
7
|
+
/** Response declarations as served, independent of their representation media or generated surface version. */
|
|
8
|
+
export declare function declaredResponseStatuses(specDir: string): Map<string, Set<string>>;
|
|
9
|
+
/** Declared schema fields, including nested objects that are not inferred as generated resources. */
|
|
10
|
+
export declare function schemaFieldNames(specDir: string): Set<string>;
|
|
7
11
|
/** The union of the documents as one OpenAPI document; each contradiction left unpatched is named, by pointer. */
|
|
8
12
|
export declare function mergeDocuments(docs: SpecDocuments): {
|
|
9
13
|
doc: Json;
|
|
@@ -9,6 +9,13 @@ import { parse as parseYaml } from 'yaml';
|
|
|
9
9
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { gunzipSync } from 'node:zlib';
|
|
12
|
+
import { applySpecPatch } from "./spec-patches.js";
|
|
13
|
+
import { slug } from "./spec-ir.js";
|
|
14
|
+
function readDocument(file) {
|
|
15
|
+
const bytes = readFileSync(file);
|
|
16
|
+
const text = (file.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('utf8');
|
|
17
|
+
return /\.ya?ml(\.gz)?$/.test(file) ? parseExactJson(JSON.stringify(parseYaml(text, { intAsBigInt: true }), (_, value) => typeof value === 'bigint' ? exactInteger(String(value)) : value)) : parseExactJson(text);
|
|
18
|
+
}
|
|
12
19
|
/** The documents of `spec/openapi/`, by name, or undefined when the pack vendors one spec file. */
|
|
13
20
|
export function readSpecDocuments(specDir) {
|
|
14
21
|
const dir = join(specDir, 'openapi');
|
|
@@ -16,14 +23,114 @@ export function readSpecDocuments(specDir) {
|
|
|
16
23
|
return undefined;
|
|
17
24
|
const documents = {};
|
|
18
25
|
for (const f of readdirSync(dir).filter((f) => /\.(json|ya?ml)(\.gz)?$/.test(f)).sort()) {
|
|
19
|
-
|
|
20
|
-
const text = (f.endsWith('.gz') ? gunzipSync(bytes) : bytes).toString('utf8');
|
|
21
|
-
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);
|
|
26
|
+
documents[f.replace(/\.(json|ya?ml)(\.gz)?$/, '')] = readDocument(join(dir, f));
|
|
22
27
|
}
|
|
23
28
|
if (Object.keys(documents).length < 2)
|
|
24
29
|
throw new Error(`${dir}: one API in several documents needs at least two; a single document is spec/openapi.{json,yaml}`);
|
|
25
30
|
return { documents };
|
|
26
31
|
}
|
|
32
|
+
const openapiFiles = ['openapi.json.gz', 'openapi.json', 'openapi.yaml.gz', 'openapi.yaml'];
|
|
33
|
+
function readPatchedDocument(specDir, filenames) {
|
|
34
|
+
const file = filenames.map((name) => join(specDir, name)).find(existsSync);
|
|
35
|
+
const documents = file === undefined ? readSpecDocuments(specDir) : undefined;
|
|
36
|
+
let doc = file === undefined ? documents : readDocument(file);
|
|
37
|
+
if (doc === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
const patches = join(specDir, 'patches.json');
|
|
40
|
+
if (existsSync(patches))
|
|
41
|
+
for (const patch of readDocument(patches).patches)
|
|
42
|
+
applySpecPatch(doc, patch);
|
|
43
|
+
if (documents !== undefined) {
|
|
44
|
+
const merged = mergeDocuments(doc);
|
|
45
|
+
if (merged.contradictions.length)
|
|
46
|
+
throw new Error(`OpenAPI response declarations conflict: ${merged.contradictions.join('; ')}`);
|
|
47
|
+
doc = merged.doc;
|
|
48
|
+
}
|
|
49
|
+
return doc;
|
|
50
|
+
}
|
|
51
|
+
/** Response declarations as served, independent of their representation media or generated surface version. */
|
|
52
|
+
export function declaredResponseStatuses(specDir) {
|
|
53
|
+
const doc = readPatchedDocument(specDir, openapiFiles);
|
|
54
|
+
const statuses = new Map();
|
|
55
|
+
if (doc === undefined)
|
|
56
|
+
return statuses;
|
|
57
|
+
for (const [path, item] of Object.entries(doc.paths ?? {})) {
|
|
58
|
+
for (const [method, operation] of Object.entries(item)) {
|
|
59
|
+
if (!operation || typeof operation !== 'object' || !operation.responses)
|
|
60
|
+
continue;
|
|
61
|
+
statuses.set(operation.operationId || slug(method, path), new Set(Object.keys(operation.responses)));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return statuses;
|
|
65
|
+
}
|
|
66
|
+
/** Declared schema fields, including nested objects that are not inferred as generated resources. */
|
|
67
|
+
export function schemaFieldNames(specDir) {
|
|
68
|
+
const doc = readPatchedDocument(specDir, ['smithy.json.gz', 'smithy.json', ...openapiFiles, 'discovery.json.gz', 'discovery.json']);
|
|
69
|
+
const fields = new Set();
|
|
70
|
+
if (doc === undefined)
|
|
71
|
+
return fields;
|
|
72
|
+
for (const shape of Object.values(doc.shapes ?? {})) {
|
|
73
|
+
if (shape.type !== 'structure' && shape.type !== 'union')
|
|
74
|
+
continue;
|
|
75
|
+
for (const [name, member] of Object.entries(shape.members ?? {})) {
|
|
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')
|
|
80
|
+
fields.add(wireName);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const schema = (node) => {
|
|
85
|
+
if (Array.isArray(node)) {
|
|
86
|
+
for (const item of node)
|
|
87
|
+
schema(item);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!node || typeof node !== 'object')
|
|
91
|
+
return;
|
|
92
|
+
for (const [name, property] of Object.entries(node.properties ?? {})) {
|
|
93
|
+
fields.add(name);
|
|
94
|
+
if (typeof property?.xml?.name === 'string')
|
|
95
|
+
fields.add(property.xml.name);
|
|
96
|
+
schema(property);
|
|
97
|
+
}
|
|
98
|
+
for (const keyword of ['$defs', 'definitions', 'patternProperties', 'dependentSchemas']) {
|
|
99
|
+
for (const child of Object.values(node[keyword] ?? {}))
|
|
100
|
+
schema(child);
|
|
101
|
+
}
|
|
102
|
+
for (const keyword of ['items', 'additionalItems', 'additionalProperties', 'unevaluatedProperties', 'unevaluatedItems', 'not', 'contains', 'propertyNames', 'if', 'then', 'else', 'allOf', 'anyOf', 'oneOf', 'prefixItems'])
|
|
103
|
+
schema(node[keyword]);
|
|
104
|
+
};
|
|
105
|
+
for (const catalog of [doc.components?.schemas, doc.definitions, doc.schemas]) {
|
|
106
|
+
for (const node of Object.values(catalog ?? {}))
|
|
107
|
+
schema(node);
|
|
108
|
+
}
|
|
109
|
+
const body = (node) => {
|
|
110
|
+
schema(node?.schema);
|
|
111
|
+
for (const media of Object.values(node?.content ?? {}))
|
|
112
|
+
schema(media?.schema);
|
|
113
|
+
};
|
|
114
|
+
for (const catalog of [doc.components?.responses, doc.components?.requestBodies, doc.components?.parameters, doc.components?.headers, doc.responses, doc.parameters]) {
|
|
115
|
+
for (const node of Object.values(catalog ?? {}))
|
|
116
|
+
body(node);
|
|
117
|
+
}
|
|
118
|
+
for (const paths of [doc.paths, doc.webhooks, doc.components?.pathItems]) {
|
|
119
|
+
for (const item of Object.values(paths ?? {})) {
|
|
120
|
+
for (const parameter of item.parameters ?? [])
|
|
121
|
+
body(parameter);
|
|
122
|
+
for (const method of ['get', 'put', 'post', 'delete', 'patch', 'head', 'options', 'trace']) {
|
|
123
|
+
const operation = item[method];
|
|
124
|
+
for (const parameter of operation?.parameters ?? [])
|
|
125
|
+
body(parameter);
|
|
126
|
+
body(operation?.requestBody);
|
|
127
|
+
for (const response of Object.values(operation?.responses ?? {}))
|
|
128
|
+
body(response);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return fields;
|
|
133
|
+
}
|
|
27
134
|
/** A node without its words (description, summary, examples, and the tags a document files it under): what two
|
|
28
135
|
* documents must agree on. */
|
|
29
136
|
const substance = (n) => 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));
|