@volter/twin-standard 1.0.158 → 1.0.159

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +3 -2
  2. package/client-sdks/package.json +1 -0
  3. package/client-sdks/pnpm-lock.yaml +210 -0
  4. package/dist/client-sdks/package.json +1 -0
  5. package/dist/client-sdks/pnpm-lock.yaml +210 -0
  6. package/dist/src/assessment.d.ts +13 -3
  7. package/dist/src/assessment.js +10 -6
  8. package/dist/src/browser-assessment.js +16 -13
  9. package/dist/src/clients/harness.d.ts +25 -4
  10. package/dist/src/clients/harness.js +184 -22
  11. package/dist/src/clients/jira.js +11 -2
  12. package/dist/src/clients/run.js +19 -14
  13. package/dist/src/conformance.d.ts +2 -0
  14. package/dist/src/conformance.js +392 -184
  15. package/dist/src/consumer.d.ts +22 -1
  16. package/dist/src/consumer.js +91 -26
  17. package/dist/src/credential-issuer-import.d.ts +3 -0
  18. package/dist/src/credential-issuer-import.js +42 -0
  19. package/dist/src/derive.js +3 -0
  20. package/dist/src/gate.js +4 -0
  21. package/dist/src/lanes.d.ts +2 -0
  22. package/dist/src/lanes.js +7 -0
  23. package/dist/src/protocol-3.js +8 -3
  24. package/dist/src/spec-documents.d.ts +4 -0
  25. package/dist/src/spec-documents.js +110 -3
  26. package/dist/src/tools/life-coverage-walk.js +38 -26
  27. package/dist/src/tools/score-pack.js +6 -5
  28. package/dist/src/tools/vendor-examples.js +91 -80
  29. package/dist/src/walk/behavior-journey.d.ts +22 -14
  30. package/dist/src/walk/behavior-journey.js +182 -133
  31. package/dist/src/walk/journey-kit.d.ts +3 -2
  32. package/dist/src/walk/journey-kit.js +6 -2
  33. package/dist/src/walk/replay.d.ts +23 -0
  34. package/dist/src/walk/replay.js +118 -0
  35. package/dist/src/walk/serve-pack.d.ts +2 -2
  36. package/dist/src/walk/serve-pack.js +38 -2
  37. package/dist/src/walk/shape.d.ts +25 -4
  38. package/dist/src/walk/shape.js +166 -56
  39. package/dist/src/walk/vendor-fetch.d.ts +7 -2
  40. package/dist/src/walk/vendor-fetch.js +13 -2
  41. package/dist/src/walk/walk-database.d.ts +9 -1
  42. package/dist/src/walk/walk-database.js +35 -13
  43. package/dist/src/walk/xml-view.d.ts +3 -2
  44. package/dist/src/walk/xml-view.js +5 -4
  45. package/package.json +3 -3
  46. package/src/assessment.ts +12 -8
  47. package/src/browser-assessment.ts +12 -5
  48. package/src/clients/harness.ts +142 -19
  49. package/src/clients/jira.ts +4 -2
  50. package/src/clients/run.ts +17 -15
  51. package/src/conformance.ts +313 -128
  52. package/src/consumer.ts +83 -21
  53. package/src/credential-issuer-import.ts +38 -0
  54. package/src/derive.ts +3 -1
  55. package/src/gate.ts +3 -0
  56. package/src/lanes.ts +7 -0
  57. package/src/protocol-3.ts +7 -3
  58. package/src/spec-documents.ts +92 -3
  59. package/src/tools/life-coverage-walk.ts +32 -22
  60. package/src/tools/score-pack.ts +7 -6
  61. package/src/tools/vendor-examples.ts +88 -75
  62. package/src/walk/behavior-journey.ts +145 -109
  63. package/src/walk/journey-kit.ts +7 -5
  64. package/src/walk/replay.ts +103 -0
  65. package/src/walk/serve-pack.ts +25 -5
  66. package/src/walk/shape.ts +156 -54
  67. package/src/walk/vendor-fetch.ts +16 -4
  68. package/src/walk/walk-database.ts +41 -15
  69. package/src/walk/xml-view.ts +5 -4
@@ -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: boolean;
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 {};
@@ -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
- for (const [name, version] of Object.entries({ ...app.dependencies, ...app.devDependencies, ...app.optionalDependencies })) {
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
- for (const [name, identity] of [[metadata.name, candidate], ['@volter/world-core', kernel]]) {
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 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 };
151
- const facts = JSON.parse(readFileSync(join(packDir, 'generated/pack-facts.json'), 'utf8'));
152
- const vendor = Object.keys(facts.packs ?? {})[0];
153
- if (!vendor || Object.keys(facts.packs).length !== 1)
154
- throw new Error('Consumer workflow requires one selected artifact identity');
155
- const recipe = consumerRecipe(vendor, packDir);
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 new Promise((done) => {
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
+ }
@@ -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}`);
@@ -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')))
@@ -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 and the shared helpers; each handler file is its family\'s and exports only its operations, named by the fixed rule',
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
- const bytes = readFileSync(join(dir, f));
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));