@volter/twin-standard 1.0.158 → 1.0.160
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/check-sources.js +2 -2
- 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/resend.first-use.json +2 -2
- package/dist/src/clients/run.js +19 -14
- package/dist/src/conformance.d.ts +3 -4
- package/dist/src/conformance.js +396 -203
- package/dist/src/consumer.d.ts +22 -1
- package/dist/src/consumer.js +91 -26
- package/dist/src/create-pack.d.ts +1 -1
- package/dist/src/create-pack.js +13 -12
- package/dist/src/credential-issuer-import.d.ts +3 -0
- package/dist/src/credential-issuer-import.js +42 -0
- package/dist/src/derive.js +6 -3
- package/dist/src/gate.js +4 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/lanes.d.ts +2 -0
- package/dist/src/lanes.js +7 -0
- package/dist/src/pack-facts.d.ts +3 -0
- package/dist/src/pack-facts.js +36 -1
- package/dist/src/protocol-3.js +13 -8
- package/dist/src/spec-documents.d.ts +4 -0
- package/dist/src/spec-documents.js +110 -3
- package/dist/src/spec-ir-client.js +1 -1
- package/dist/src/spec-ir-commands.js +1 -1
- package/dist/src/spec-ir-lines.js +1 -1
- package/dist/src/spec-ir-proto.d.ts +1 -1
- package/dist/src/spec-patches.js +2 -2
- package/dist/src/tools/judge-brief.js +1 -2
- package/dist/src/tools/life-coverage-walk.js +38 -26
- package/dist/src/tools/life-coverage.js +2 -2
- package/dist/src/tools/packs-root.js +3 -3
- package/dist/src/tools/score-pack.js +7 -7
- package/dist/src/tools/vendor-examples.js +95 -85
- package/dist/src/verdict.js +1 -1
- package/dist/src/walk/behavior-journey.d.ts +22 -14
- package/dist/src/walk/behavior-journey.js +199 -133
- package/dist/src/walk/journey-kit.d.ts +5 -4
- 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 +44 -2
- package/dist/src/walk/shape.d.ts +26 -5
- package/dist/src/walk/shape.js +167 -57
- 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 +6 -4
- package/src/assessment.ts +12 -8
- package/src/browser-assessment.ts +12 -5
- package/src/check-sources.ts +2 -2
- package/src/clients/harness.ts +142 -19
- package/src/clients/jira.ts +4 -2
- package/src/clients/resend.first-use.json +2 -2
- package/src/clients/run.ts +17 -15
- package/src/conformance.ts +317 -147
- package/src/consumer.ts +83 -21
- package/src/create-pack.ts +13 -12
- package/src/credential-issuer-import.ts +38 -0
- package/src/derive.ts +6 -4
- package/src/gate.ts +3 -0
- package/src/index.ts +1 -1
- package/src/lanes.ts +7 -0
- package/src/pack-facts.ts +25 -2
- package/src/protocol-3.ts +12 -8
- package/src/spec-documents.ts +92 -3
- package/src/spec-ir-client.ts +1 -1
- package/src/spec-ir-commands.ts +1 -1
- package/src/spec-ir-lines.ts +1 -1
- package/src/spec-ir-proto.ts +1 -1
- package/src/spec-patches.ts +2 -2
- package/src/tools/judge-brief.ts +1 -2
- package/src/tools/life-coverage-walk.ts +32 -22
- package/src/tools/life-coverage.ts +2 -2
- package/src/tools/packs-root.ts +3 -3
- package/src/tools/score-pack.ts +8 -8
- package/src/tools/vendor-examples.ts +92 -80
- package/src/verdict.ts +1 -1
- package/src/walk/behavior-journey.ts +158 -109
- package/src/walk/journey-kit.ts +9 -7
- package/src/walk/replay.ts +103 -0
- package/src/walk/serve-pack.ts +30 -5
- package/src/walk/shape.ts +157 -55
- 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
|
+
}
|
|
@@ -25,7 +25,7 @@ export declare function brief(unit: string): Promise<string>;
|
|
|
25
25
|
export declare function packIndexFile(dir: string, vendor: string): string;
|
|
26
26
|
/** A pack's `src/server.ts` (a fixed file), the kernel's serve seam around its fetch, naming only what its unit has so
|
|
27
27
|
* it typechecks and a bundler follows every import: the plain seam (serveHttp, which takes the sockets a fetch carries
|
|
28
|
-
* itself: "Other wires"), or, for a unit whose spec is a proto, the gRPC seam over the schema
|
|
28
|
+
* itself: "Other wires"), or, for a unit whose spec is a proto, the gRPC seam over the schema the derivation generated
|
|
29
29
|
* (src/generated/proto.gen.json, imported by its literal path). `--init` writes the plain one, `--index` the gRPC one once
|
|
30
30
|
* the unit's proto is derived. */
|
|
31
31
|
export declare function serverFile(vendor: string, grpc: boolean, wire?: boolean, live?: boolean): string;
|
package/dist/src/create-pack.js
CHANGED
|
@@ -10,8 +10,9 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
10
10
|
// create-pack — the files of a Protocol 3 pack that nobody should write (the board's `create-pack` card;
|
|
11
11
|
// docs/contributing/architecture.md, "Creating a pack"), and the brief an author writes the rest from.
|
|
12
12
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
13
|
+
// twin-standard create <dir> --init <vendor> --package <@scope/name>
|
|
14
|
+
// write every fixed file of a new pack that does not exist yet
|
|
15
|
+
// twin-standard create <dir> --init <vendor> --package <@scope/name> --lanes a,b
|
|
15
16
|
// the same for a pack of lanes, one per API of the vendor, each
|
|
16
17
|
// derived from its own spec (<dir>/<lane>/spec): each lane's
|
|
17
18
|
// manifest, fetch, index and semantics, and the pack's server
|
|
@@ -20,11 +21,11 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
20
21
|
// index}.ts, src/semantics/{states,shared,doors}.ts,
|
|
21
22
|
// journeys/{demand,first-use}.json): the author fills the manifest's
|
|
22
23
|
// vendor facts, the descriptor's hosts and the families
|
|
23
|
-
//
|
|
24
|
+
// twin-standard create <unit> --index write src/semantics/index.ts, the handler map, and src/fetch.ts from the
|
|
24
25
|
// families' files (every exported function, by its name)
|
|
25
|
-
//
|
|
26
|
+
// twin-standard create <unit> --index --building the same during construction; unfinished operations stay gaps,
|
|
26
27
|
// and implemented operations must match their decisions
|
|
27
|
-
//
|
|
28
|
+
// twin-standard create <unit> --brief [--out f] the author's brief: every operation the derived core does not
|
|
28
29
|
// serve and no handler answers yet, by family, with what the
|
|
29
30
|
// vendored spec says of it (summary, description, parameters,
|
|
30
31
|
// the answers it documents and their examples), so a handler
|
|
@@ -75,7 +76,7 @@ export function handlerIndex(dir) {
|
|
|
75
76
|
// an index written before the derive has no operations to map, and every served call would fall to the gap (404): refuse
|
|
76
77
|
// it rather than write an empty map (2026-09-30: six packs committed so)
|
|
77
78
|
if (!existsSync(surfacePath) && fams.length)
|
|
78
|
-
throw new Error(`${dir}: no src/generated/surface.gen.json yet; derive the unit (
|
|
79
|
+
throw new Error(`${dir}: no src/generated/surface.gen.json yet; derive the unit (twin-standard derive) before --index`);
|
|
79
80
|
const where = new Map(fams.flatMap((f) => f.handlers.map((h) => [h, f.family])));
|
|
80
81
|
const named = new Set([...new Set(ids)].map(handlerName));
|
|
81
82
|
const stray = [...where.keys()].filter((h) => !named.has(h));
|
|
@@ -278,7 +279,7 @@ export async function brief(unit) {
|
|
|
278
279
|
const s = spec.get(o.id);
|
|
279
280
|
lines.push(`### ${o.id}${unmodeled.has(o.id) ? ' (unmodeled)' : ''}`, '', `\`${o.method.toUpperCase()} ${o.path}\` — ${o.class}${s?.summary ? `: ${oneLine(s.summary)}` : ''}`);
|
|
280
281
|
// the citation a handler carries, ready to paste: by operationId where the spec names one, else by its JSON pointer,
|
|
281
|
-
// quoting the spec's own words (
|
|
282
|
+
// quoting the spec's own words (the derivation refuses a quote the spec does not hold)
|
|
282
283
|
const words = oneLine(s?.summary ?? s?.description ?? '').split(/(?<=\.)\s/)[0].replace(/"/g, '\\"');
|
|
283
284
|
const cite = s && s.operationId === o.id ? `spec:${o.id}` : `spec:/paths/${o.path.replace(/~/g, '~0').replace(/\//g, '~1')}/${o.method.toLowerCase()}`;
|
|
284
285
|
if (words)
|
|
@@ -343,7 +344,7 @@ export function packIndexFile(dir, vendor) {
|
|
|
343
344
|
}
|
|
344
345
|
/** A pack's `src/server.ts` (a fixed file), the kernel's serve seam around its fetch, naming only what its unit has so
|
|
345
346
|
* it typechecks and a bundler follows every import: the plain seam (serveHttp, which takes the sockets a fetch carries
|
|
346
|
-
* itself: "Other wires"), or, for a unit whose spec is a proto, the gRPC seam over the schema
|
|
347
|
+
* itself: "Other wires"), or, for a unit whose spec is a proto, the gRPC seam over the schema the derivation generated
|
|
347
348
|
* (src/generated/proto.gen.json, imported by its literal path). `--init` writes the plain one, `--index` the gRPC one once
|
|
348
349
|
* the unit's proto is derived. */
|
|
349
350
|
export function serverFile(vendor, grpc, wire = false, live = false) {
|
|
@@ -443,7 +444,7 @@ export function initFiles(vendor) {
|
|
|
443
444
|
'src/semantics/states.ts': `// ${vendor}'s state machines and state rulings, each transition cited (architecture, "State").\nexport const states = {};\n`,
|
|
444
445
|
'src/semantics/shared.ts': `// What every family of ${vendor} shares: its stored shapes, who is calling, and the vendor's answers.\nimport type { HandlerContext } from '@volter/world-core';\n\nexport type Row = Record<string, unknown>;\nexport type { HandlerContext };\n`,
|
|
445
446
|
'src/semantics/doors.ts': `// The World's doors (architecture, "Doors, screens and the gap"): what the vendor's own pages do that its API does\n// not, each declared in the manifest's \`doors\` (its id, method and path under \`/_twin/\`) and answered here by the\n// export its id names, over the contract's context.\nimport type { HandlerContext } from '@volter/world-core';\n\nexport type { HandlerContext };\n`,
|
|
446
|
-
'src/manifest.ts': `// ${vendor}'s manifest: the vendor facts its spec does not carry (architecture, "Protocol 3"). The surface is generated\n// (./generated/surface.gen.json, from ../spec by derive
|
|
447
|
+
'src/manifest.ts': `// ${vendor}'s manifest: the vendor facts its spec does not carry (architecture, "Protocol 3"). The surface is generated\n// (./generated/surface.gen.json, from ../spec by twin-standard derive). Every line marked FILL is a vendor fact to state from\n// the vendor's pages, cited.\nimport type { DerivedManifest } from '@volter/world-core';\n\nexport const manifest: DerivedManifest = {\n vendor: '${vendor}',\n service: '${vendor}',\n body: { json: 'always' },\n ids: { template: '{uuid}' }, // FILL: the vendor's id form\n time: 'iso', // FILL: iso, unix or unix-ms\n error: { error: { message: '{message}', code: '{code}' } }, // FILL: the vendor's error body\n readOnly: { status: 403, message: 'This twin was started read-only; writes are refused.' }, // FILL: the vendor's refusal\n notFound: { status: 404, message: 'Not found' }, // FILL: the vendor's not-found\n list: { style: 'envelope', envelope: { data: '{data}' }, limit: { param: 'limit', default: 20, max: 100 } }, // FILL: its paging\n deleted: {},\n resources: {}, // FILL: the resources the core serves (plain create, read, update, delete, list)\n discovery: { twinOf: 'FILL: what the twin is of', stores: 'FILL: what it keeps' },\n // the descriptor, as data (twin-world's architecture, "The descriptor"): the pack registers packOf(manifest)\n descriptor: {\n protocol: '3',\n transport: 'rest',\n archetype: 'crud',\n bin: 'world-${vendor}',\n resources: [], // FILL: the stored types\n specSource: 'FILL: the vendored spec (spec/SOURCE.md)',\n description: 'FILL: one line',\n adoption: { sdks: [], envStems: [] }, // FILL: its npm clients, and each credential variable's stem (TRIPO for TRIPO_API_KEY: the suffix removed)\n hosts: [], // FILL: the vendor's hosts\n endpointEnvNone: 'FILL: the SDK\\'s base-URL variable (endpointEnv) or why none',\n },\n};\n`,
|
|
447
448
|
'src/server.ts': serverFile(vendor, false),
|
|
448
449
|
'src/cli.ts': cliFile(vendor),
|
|
449
450
|
'src/index.ts': `// @volter/twin-${vendor} — the ${vendor} twin (Protocol 3). A fixed file: the descriptor is the manifest's.\nimport { packOf, registerPack } from '@volter/world-core';\nimport { manifest } from './manifest.ts';\n\nexport { create${Name}Fetch } from './fetch.ts';\nexport { create${Name}TwinServer } from './server.ts';\nexport { manifest };\n\n// The descriptor: twin-world's architecture, "The descriptor"; declared in manifest.ts.\nexport const pack = registerPack(packOf(manifest));\n`,
|
|
@@ -522,7 +523,7 @@ export async function fixedFiles(dir) {
|
|
|
522
523
|
throw new Error('Pack manifest must declare its vendor identity independently of the npm package name');
|
|
523
524
|
files['src/index.ts'] = packIndexFile(dir, vendor);
|
|
524
525
|
if (!existsSync(join(dir, 'src', 'stream.ts'))) {
|
|
525
|
-
// a unit whose spec is a proto (
|
|
526
|
+
// a unit whose spec is a proto (the derivation generated its schema) is served over gRPC: its serve seam in that form
|
|
526
527
|
const lanesOnly = !hasOwnSurface(dir) && readdirSync(dir).some((n) => existsSync(join(dir, n, 'src', 'manifest.ts')));
|
|
527
528
|
const grpc = !lanesOnly && existsSync(join(dir, 'src', 'generated', 'proto.gen.json'));
|
|
528
529
|
// a unit that takes broadcasts over RTMP (its manifest's `live`) has the live forms --index writes, so they reproduce
|
|
@@ -535,7 +536,7 @@ export async function fixedFiles(dir) {
|
|
|
535
536
|
export async function createPack(args) {
|
|
536
537
|
const [unit, ...rest] = args;
|
|
537
538
|
if (!unit || (!rest.includes('--index') && !rest.includes('--brief') && !rest.includes('--init'))) {
|
|
538
|
-
console.error('usage:
|
|
539
|
+
console.error('usage: twin-standard create <dir> --init <vendor> --package <@scope/name> | <unit> --index [--building] | --brief [--out <file>]');
|
|
539
540
|
process.exit(2);
|
|
540
541
|
}
|
|
541
542
|
if (rest.includes('--building') && (!rest.includes('--index') || rest.includes('--init') || rest.includes('--brief'))) {
|
|
@@ -642,7 +643,7 @@ export async function createPack(args) {
|
|
|
642
643
|
writeFileSync(join(dir, 'tsconfig.json'), TSCONFIG);
|
|
643
644
|
console.log(`create-pack: wrote ${join(dir, 'tsconfig.json')}`);
|
|
644
645
|
}
|
|
645
|
-
// a unit whose spec is a proto (
|
|
646
|
+
// a unit whose spec is a proto (the derivation generated its schema) is served over gRPC: its serve seam in that form,
|
|
646
647
|
// the one form a unit takes after --init (a server.ts of a wire of the pack's own, SMTP's, is never written over)
|
|
647
648
|
if (!lanesOnly && existsSync(join(dir, 'src', 'generated', 'proto.gen.json'))) {
|
|
648
649
|
const wire = existsSync(join(dir, 'src', 'stream.ts'));
|
|
@@ -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
|
@@ -13,7 +13,7 @@ import { parseExactJson, exactInteger } from '@volter/world-core/exact-json';
|
|
|
13
13
|
// vendored spec (`<pack>/spec/openapi.json[.gz]`), builds the spec IR (./spec-ir.ts) and writes the pack's
|
|
14
14
|
// `src/generated/`. Deterministic: a second run over the same spec writes the same bytes. Never hand-edit what it writes.
|
|
15
15
|
//
|
|
16
|
-
// Usage: twin-standard derive <pack dir> [--out <dir>]
|
|
16
|
+
// Usage: twin-standard derive <pack dir> [--out <dir>]
|
|
17
17
|
import { parse as parseYaml } from 'yaml';
|
|
18
18
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
19
19
|
import { basename, join } from 'node:path';
|
|
@@ -47,7 +47,7 @@ const PKG = unitAt(vendor).dir;
|
|
|
47
47
|
const rfc = existsSync(join(PKG, 'spec')) ? readdirSync(join(PKG, 'spec')).find((f) => /^rfc\d+\.txt$/.test(f)) : undefined;
|
|
48
48
|
const specPath = ['openapi.json.gz', 'openapi.json', 'openapi.yaml.gz', 'openapi.yaml', 'smithy.json.gz', 'smithy.json', 'discovery.json.gz', 'discovery.json', ...(rfc ? [rfc] : []), 'openapi', 'proto', 'commands', 'command-builders.json', 'client-ops.json'].map((f) => join(PKG, 'spec', f)).find((p) => existsSync(p));
|
|
49
49
|
if (!specPath) {
|
|
50
|
-
console.error(`derive
|
|
50
|
+
console.error(`derive: no spec at ${join(PKG, 'spec')}/openapi.{json,yaml}[.gz], smithy.json[.gz], rfcNNNN.txt, proto/, commands/, command-builders.json or client-ops.json`);
|
|
51
51
|
process.exit(SOURCE_REFUSAL);
|
|
52
52
|
}
|
|
53
53
|
const rpcService = basename(specPath) === 'proto';
|
|
@@ -343,7 +343,7 @@ if (existsSync(manifestPath)) {
|
|
|
343
343
|
}
|
|
344
344
|
// a resource's refresh scope ("The real-system adapters", What a pack declares) names the list that enumerates it or
|
|
345
345
|
// its retrieve, or says why the vendor offers no read-back; whether every stored resource declares one is
|
|
346
|
-
//
|
|
346
|
+
// not this build's to decide
|
|
347
347
|
// (a resource the spec names is read by its own list or retrieve; one the manifest names apart from the spec's, an RPC
|
|
348
348
|
// vendor's, by the list or read the scope names, with `items` where the answer holds them when the spec does not say)
|
|
349
349
|
const specNamed = new Set(ir.resources.map((r) => r.name));
|
|
@@ -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/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { grade, STANDARDS, type CriterionGrade, type GradeOptions, type StandardGrade } from './grade.js';
|
|
2
|
-
export { conformance, decisionTableProblems, type ConformanceResult, UNIT_FILES
|
|
2
|
+
export { conformance, decisionTableProblems, type ConformanceResult, UNIT_FILES } from './conformance.js';
|
|
3
3
|
export { laneRootFailures, p3Gate, unitsOf } from './gate.js';
|
|
4
4
|
export { conformPublished, gatePublished, vendorOfPackage, type PublishedGate } from './published.js';
|
|
5
5
|
export { handlerName, protocol3, PLANNED, withCheckoutFiles } from './protocol-3.js';
|
package/dist/src/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// of a submission — judges by the same standard at the version it pins. Changing a criterion is a release of this
|
|
5
5
|
// package: a criterion's id never changes meaning, a changed criterion takes a new id and a new standard version.
|
|
6
6
|
export { grade, STANDARDS } from "./grade.js";
|
|
7
|
-
export { conformance, decisionTableProblems, UNIT_FILES
|
|
7
|
+
export { conformance, decisionTableProblems, UNIT_FILES } from "./conformance.js";
|
|
8
8
|
export { laneRootFailures, p3Gate, unitsOf } from "./gate.js";
|
|
9
9
|
export { conformPublished, gatePublished, vendorOfPackage } from "./published.js";
|
|
10
10
|
export { handlerName, protocol3, PLANNED, withCheckoutFiles } from "./protocol-3.js";
|
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/pack-facts.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export type PackFacts = {
|
|
|
29
29
|
demandPathPattern?: string | false;
|
|
30
30
|
key?: string;
|
|
31
31
|
exclude?: true;
|
|
32
|
+
basic?: string;
|
|
32
33
|
}>;
|
|
33
34
|
hostsNone?: string;
|
|
34
35
|
hostsClaimed?: {
|
|
@@ -104,6 +105,8 @@ export type PackFacts = {
|
|
|
104
105
|
scheme?: string;
|
|
105
106
|
note: string;
|
|
106
107
|
}>;
|
|
108
|
+
/** The vendor's own requests a World's generated seed makes (see TwinPack.seed). */
|
|
109
|
+
seed?: TwinPack['seed'];
|
|
107
110
|
/** DERIVED, never declared: the pack's `create<Name>TwinServer` factory export — the
|
|
108
111
|
* `({port,root,readOnly}) => {port,stop}` contract the colocated host mounts
|
|
109
112
|
* (world-runtime/src/host.ts). Read off the module's own exports because the name is not
|
package/dist/src/pack-facts.js
CHANGED
|
@@ -42,6 +42,8 @@ function validateHostRules(vendor, rules, vendors, claimedKeys, worldIds) {
|
|
|
42
42
|
}
|
|
43
43
|
if (rule.exclude !== undefined && rule.exclude !== true)
|
|
44
44
|
throw new Error(`pack-facts: ${vendor} host rule ${JSON.stringify(rule)} — exclude is \`true\` or absent, never a falsy flag`);
|
|
45
|
+
if (rule.basic !== undefined && (typeof rule.basic !== 'string' || !/^[^:\s]+$/.test(rule.basic) || rule.exclude))
|
|
46
|
+
throw new Error(`pack-facts: ${vendor} host rule ${JSON.stringify(rule)} — basic names a user (no colon, no space) on an included host`);
|
|
45
47
|
const key = rule.key ?? vendor;
|
|
46
48
|
// A foreign key is a WORLD identity too: `volter-world init` emits <KEY>_TWIN_URL only for the
|
|
47
49
|
// ids in adoption.worldIds (covers.ts injectorVendorKeysFor), so a key not listed there would
|
|
@@ -230,6 +232,39 @@ export async function compilePackFacts(dirs) {
|
|
|
230
232
|
}
|
|
231
233
|
facts.sidePorts = pack.sidePorts;
|
|
232
234
|
}
|
|
235
|
+
if (pack.seed !== undefined) {
|
|
236
|
+
// ADR 0033 §3: one door per kind of fact, each the vendor's own request at its own host; only a domain's DNS record
|
|
237
|
+
// is a `/_twin/` stand-in
|
|
238
|
+
const seed = pack.seed;
|
|
239
|
+
const door = (value, what, standIn = false) => {
|
|
240
|
+
let url;
|
|
241
|
+
try {
|
|
242
|
+
url = new URL(String(value));
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
throw new Error(`pack-facts: ${vendor} seed ${what} "${String(value)}" is not a URL`);
|
|
246
|
+
}
|
|
247
|
+
if (url.protocol !== 'https:')
|
|
248
|
+
throw new Error(`pack-facts: ${vendor} seed ${what} is the vendor's own https request`);
|
|
249
|
+
if (!standIn && url.pathname.startsWith('/_twin/'))
|
|
250
|
+
throw new Error(`pack-facts: ${vendor} seed ${what} is the vendor's own request, never a /_twin/ door`);
|
|
251
|
+
};
|
|
252
|
+
if (!seed || typeof seed !== 'object' || Array.isArray(seed) || Object.keys(seed).some((k) => k !== 'signUp' && k !== 'domain'))
|
|
253
|
+
throw new Error(`pack-facts: ${vendor} seed is { signUp?, domain? }`);
|
|
254
|
+
if (seed.signUp !== undefined)
|
|
255
|
+
door(seed.signUp, 'signUp');
|
|
256
|
+
if (seed.domain !== undefined) {
|
|
257
|
+
const domain = seed.domain;
|
|
258
|
+
if (!domain || typeof domain !== 'object' || Array.isArray(domain) || Object.keys(domain).sort().join() !== 'add,record,settles,verify')
|
|
259
|
+
throw new Error(`pack-facts: ${vendor} seed.domain is { add, record, verify, settles }`);
|
|
260
|
+
door(domain.add, 'domain.add');
|
|
261
|
+
door(domain.verify, 'domain.verify');
|
|
262
|
+
door(domain.record, 'domain.record', true);
|
|
263
|
+
if (typeof domain.settles !== 'string' || !/^\d+[smhd]$/.test(domain.settles))
|
|
264
|
+
throw new Error(`pack-facts: ${vendor} seed.domain.settles is a World-clock time (10m)`);
|
|
265
|
+
}
|
|
266
|
+
facts.seed = pack.seed;
|
|
267
|
+
}
|
|
233
268
|
// The serve factory is DERIVED from the module's exports (see PackFacts.serveExport);
|
|
234
269
|
// a pack exporting SEVERAL factories must declare which one `cli.ts serve` boots — the
|
|
235
270
|
// exports alone cannot reveal that judgment (linear defaults to its DERIVED server).
|
|
@@ -250,7 +285,7 @@ export async function compilePackFacts(dirs) {
|
|
|
250
285
|
packs[vendor] = facts;
|
|
251
286
|
}
|
|
252
287
|
return {
|
|
253
|
-
'//': 'GENERATED by
|
|
288
|
+
'//': 'GENERATED by twin-standard facts from the pack descriptor; no prior-artifact entries are retained.',
|
|
254
289
|
packs: sortObject(packs),
|
|
255
290
|
undeclared,
|
|
256
291
|
};
|