@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.
Files changed (100) 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/check-sources.js +2 -2
  10. package/dist/src/clients/harness.d.ts +25 -4
  11. package/dist/src/clients/harness.js +184 -22
  12. package/dist/src/clients/jira.js +11 -2
  13. package/dist/src/clients/resend.first-use.json +2 -2
  14. package/dist/src/clients/run.js +19 -14
  15. package/dist/src/conformance.d.ts +3 -4
  16. package/dist/src/conformance.js +396 -203
  17. package/dist/src/consumer.d.ts +22 -1
  18. package/dist/src/consumer.js +91 -26
  19. package/dist/src/create-pack.d.ts +1 -1
  20. package/dist/src/create-pack.js +13 -12
  21. package/dist/src/credential-issuer-import.d.ts +3 -0
  22. package/dist/src/credential-issuer-import.js +42 -0
  23. package/dist/src/derive.js +6 -3
  24. package/dist/src/gate.js +4 -0
  25. package/dist/src/index.d.ts +1 -1
  26. package/dist/src/index.js +1 -1
  27. package/dist/src/lanes.d.ts +2 -0
  28. package/dist/src/lanes.js +7 -0
  29. package/dist/src/pack-facts.d.ts +3 -0
  30. package/dist/src/pack-facts.js +36 -1
  31. package/dist/src/protocol-3.js +13 -8
  32. package/dist/src/spec-documents.d.ts +4 -0
  33. package/dist/src/spec-documents.js +110 -3
  34. package/dist/src/spec-ir-client.js +1 -1
  35. package/dist/src/spec-ir-commands.js +1 -1
  36. package/dist/src/spec-ir-lines.js +1 -1
  37. package/dist/src/spec-ir-proto.d.ts +1 -1
  38. package/dist/src/spec-patches.js +2 -2
  39. package/dist/src/tools/judge-brief.js +1 -2
  40. package/dist/src/tools/life-coverage-walk.js +38 -26
  41. package/dist/src/tools/life-coverage.js +2 -2
  42. package/dist/src/tools/packs-root.js +3 -3
  43. package/dist/src/tools/score-pack.js +7 -7
  44. package/dist/src/tools/vendor-examples.js +95 -85
  45. package/dist/src/verdict.js +1 -1
  46. package/dist/src/walk/behavior-journey.d.ts +22 -14
  47. package/dist/src/walk/behavior-journey.js +199 -133
  48. package/dist/src/walk/journey-kit.d.ts +5 -4
  49. package/dist/src/walk/journey-kit.js +6 -2
  50. package/dist/src/walk/replay.d.ts +23 -0
  51. package/dist/src/walk/replay.js +118 -0
  52. package/dist/src/walk/serve-pack.d.ts +2 -2
  53. package/dist/src/walk/serve-pack.js +44 -2
  54. package/dist/src/walk/shape.d.ts +26 -5
  55. package/dist/src/walk/shape.js +167 -57
  56. package/dist/src/walk/vendor-fetch.d.ts +7 -2
  57. package/dist/src/walk/vendor-fetch.js +13 -2
  58. package/dist/src/walk/walk-database.d.ts +9 -1
  59. package/dist/src/walk/walk-database.js +35 -13
  60. package/dist/src/walk/xml-view.d.ts +3 -2
  61. package/dist/src/walk/xml-view.js +5 -4
  62. package/package.json +6 -4
  63. package/src/assessment.ts +12 -8
  64. package/src/browser-assessment.ts +12 -5
  65. package/src/check-sources.ts +2 -2
  66. package/src/clients/harness.ts +142 -19
  67. package/src/clients/jira.ts +4 -2
  68. package/src/clients/resend.first-use.json +2 -2
  69. package/src/clients/run.ts +17 -15
  70. package/src/conformance.ts +317 -147
  71. package/src/consumer.ts +83 -21
  72. package/src/create-pack.ts +13 -12
  73. package/src/credential-issuer-import.ts +38 -0
  74. package/src/derive.ts +6 -4
  75. package/src/gate.ts +3 -0
  76. package/src/index.ts +1 -1
  77. package/src/lanes.ts +7 -0
  78. package/src/pack-facts.ts +25 -2
  79. package/src/protocol-3.ts +12 -8
  80. package/src/spec-documents.ts +92 -3
  81. package/src/spec-ir-client.ts +1 -1
  82. package/src/spec-ir-commands.ts +1 -1
  83. package/src/spec-ir-lines.ts +1 -1
  84. package/src/spec-ir-proto.ts +1 -1
  85. package/src/spec-patches.ts +2 -2
  86. package/src/tools/judge-brief.ts +1 -2
  87. package/src/tools/life-coverage-walk.ts +32 -22
  88. package/src/tools/life-coverage.ts +2 -2
  89. package/src/tools/packs-root.ts +3 -3
  90. package/src/tools/score-pack.ts +8 -8
  91. package/src/tools/vendor-examples.ts +92 -80
  92. package/src/verdict.ts +1 -1
  93. package/src/walk/behavior-journey.ts +158 -109
  94. package/src/walk/journey-kit.ts +9 -7
  95. package/src/walk/replay.ts +103 -0
  96. package/src/walk/serve-pack.ts +30 -5
  97. package/src/walk/shape.ts +157 -55
  98. package/src/walk/vendor-fetch.ts +16 -4
  99. package/src/walk/walk-database.ts +41 -15
  100. package/src/walk/xml-view.ts +5 -4
package/src/consumer.ts CHANGED
@@ -19,13 +19,28 @@ export type ConsumerRecipe = {
19
19
  };
20
20
  export type ConsumerOptions = { appDir: string; cli: string; artifactIntegrity: string };
21
21
  type ConsumerInstallation = { cli: string; artifact?: string };
22
+ export type ConsumerResult = Awaited<ReturnType<typeof runConsumerWorkflow>>;
23
+
24
+ function recipeDependencies(recipe: ConsumerRecipe): Record<string, string> {
25
+ const app = JSON.parse(recipe.files['package.json']!);
26
+ const dependencies = { ...app.dependencies, ...app.devDependencies, ...app.optionalDependencies };
27
+ for (const [name, version] of Object.entries(dependencies)) {
28
+ if (typeof version !== 'string' || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) throw new Error(`${name}: customer SDK dependencies require exact registry versions`);
29
+ }
30
+ return dependencies;
31
+ }
22
32
 
23
33
  function packageIdentity(entry: string, name: string): { directory: string; version: string } {
24
34
  let directory = dirname(realpathSync(entry));
25
35
  while (true) {
26
- const file = join(directory, 'package.json');
27
- if (existsSync(file)) { const pkg = JSON.parse(readFileSync(file, 'utf8')); if (pkg.name === name) return { directory, version: pkg.version }; }
28
36
  const parent = dirname(directory);
37
+ // The installed node_modules slot owns the entry; nested distribution manifests do not identify the package.
38
+ const boundary = basename(parent) === 'node_modules' || (basename(parent).startsWith('@') && basename(dirname(parent)) === 'node_modules');
39
+ if (boundary) {
40
+ const pkg = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));
41
+ if (pkg.name !== name) throw new Error(`${name}: installed package boundary belongs to ${String(pkg.name)}`);
42
+ return { directory, version: pkg.version };
43
+ }
29
44
  if (parent === directory) throw new Error(`${name} has no installed package identity`);
30
45
  directory = parent;
31
46
  }
@@ -39,6 +54,21 @@ function consumerTools(cli: string) {
39
54
  };
40
55
  }
41
56
 
57
+ function consumerEntries(directory: string, names: string[]): Map<string, string> {
58
+ if (!names.length) return new Map();
59
+ 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' });
60
+ if (result.error || result.status !== 0) throw new Error(`Consumer ESM dependency resolution failed: ${result.error?.message ?? result.stderr.trim()}`);
61
+ const entries = JSON.parse(result.stdout) as string[];
62
+ return new Map(names.map((name, index) => [name, fileURLToPath(entries[index]!)]));
63
+ }
64
+
65
+ function consumerPeerVersions(metadata: { dependencies?: Record<string, string>; optionalDependencies?: Record<string, string>; peerDependencies?: Record<string, string>; peerDependenciesMeta?: Record<string, { optional?: boolean }> }, cli: string, sdkDependencies: Record<string, string>): Record<string, string> {
66
+ const names = Object.keys(metadata.peerDependencies ?? {})
67
+ .filter((name) => name !== '@volter/world-core' && !metadata.peerDependenciesMeta?.[name]?.optional && !metadata.dependencies?.[name] && !metadata.optionalDependencies?.[name] && !sdkDependencies[name]);
68
+ const entries = consumerEntries(dirname(realpathSync(cli)), names);
69
+ return Object.fromEntries(names.map((name) => [name, packageIdentity(entries.get(name)!, name).version]));
70
+ }
71
+
42
72
  /** Bind behavior inputs on both source and packed layouts; compiled outputs and score caches are not inputs. */
43
73
  export function consumerSourceDigest(pack: string): string {
44
74
  const entries: Array<[string, string]> = [];
@@ -83,9 +113,7 @@ export function prepareConsumerApp(packDir: string, appDir: string, installation
83
113
  app.packageManager = 'pnpm@12.15.0';
84
114
  app.devEngines = { packageManager: { name: 'pnpm', version: '12.15.0', onFail: 'error' } };
85
115
  if (app.scripts || app.workspaces) throw new Error('Consumer fixture supplies direct SDK commands, not install scripts or workspace links');
86
- for (const [name, version] of Object.entries({ ...app.dependencies, ...app.devDependencies, ...app.optionalDependencies })) {
87
- if (typeof version !== 'string' || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) throw new Error(`${name}: customer SDK dependencies require exact registry versions`);
88
- }
116
+ const sdkDependencies = recipeDependencies(recipe);
89
117
  const files = Object.entries(recipe.files).map(([path, content]) => {
90
118
  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)) throw new Error(`Invalid consumer file: ${path}`);
91
119
  if (typeof content !== 'string') throw new Error('Consumer files must contain text');
@@ -96,7 +124,7 @@ export function prepareConsumerApp(packDir: string, appDir: string, installation
96
124
  const tools = consumerTools(installation.cli);
97
125
  if (installation.artifact && (!isAbsolute(installation.artifact) || !statSync(installation.artifact).isFile())) throw new Error('Consumer candidate must be an absolute prepared archive path');
98
126
  if (Object.hasOwn(app.dependencies ?? {}, metadata.name)) throw new Error('Consumer SDK dependencies must not substitute the candidate package');
99
- app.devDependencies = { ...app.devDependencies, '@volter/world-core': tools['@volter/world-core'], [metadata.name]: installation.artifact ? `file:${installation.artifact}` : metadata.version };
127
+ 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 };
100
128
  }
101
129
  files.find(([path]) => path === 'package.json')![1] = JSON.stringify(app, null, 2) + '\n';
102
130
  files.push(['pnpm-workspace.yaml', 'packages:\n - .\npackageImportMethod: hardlink\nnodeLinker: hoisted\nautoInstallPeers: false\nstrictPeerDependencies: false\n']);
@@ -118,40 +146,56 @@ export function prepareConsumerApp(packDir: string, appDir: string, installation
118
146
  return { recipeSha256: createHash('sha256').update(JSON.stringify(recipe)).digest('hex'), dependencies: app.dependencies ?? {} };
119
147
  }
120
148
 
149
+ /** Run the unchanged customer command, with the environment supplied by its World. */
150
+ export function runConsumerCommand(argv: string[], appDir: string, env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<{ exitCode: number | null; stdout: string; stderr: string }> {
151
+ return new Promise((done) => {
152
+ const child = spawn(argv[0]!, argv.slice(1), { cwd: appDir, env, signal, stdio: ['ignore', 'pipe', 'pipe'] });
153
+ let stdout = '', stderr = '';
154
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
155
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
156
+ child.on('error', (error) => { stderr += `\n${error.message}`; });
157
+ child.on('close', (exitCode) => done({ exitCode, stdout, stderr }));
158
+ });
159
+ }
160
+
121
161
  export async function runConsumerWorkflow(packDir: string, options: ConsumerOptions) {
122
162
  if (!isAbsolute(options.appDir) || !isAbsolute(options.cli) || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(options.artifactIntegrity)) throw new Error('Consumer requires absolute prepared paths and candidate SHA-512 integrity');
123
163
  if (existsSync(join(options.appDir, '.volter/world.json'))) throw new Error('Consumer app already has a World; prepare a new app rather than reusing prior state');
124
164
  const cliEntry = realpathSync(options.cli);
125
165
  const tools = consumerTools(cliEntry);
126
166
  const metadata = JSON.parse(readFileSync(join(packDir, 'package.json'), 'utf8'));
167
+ const facts = JSON.parse(readFileSync(join(packDir, 'generated/pack-facts.json'), 'utf8'));
168
+ const vendor = Object.keys(facts.packs ?? {})[0];
169
+ if (!vendor || Object.keys(facts.packs).length !== 1) throw new Error('Consumer workflow requires one selected artifact identity');
170
+ const recipe = consumerRecipe(vendor, packDir), sdkDependencies = recipeDependencies(recipe);
127
171
  const appRoot = realpathSync(options.appDir), appRequire = createRequire(join(appRoot, 'package.json'));
128
172
  const candidate = packageIdentity(appRequire.resolve(metadata.name), metadata.name);
129
173
  const kernel = packageIdentity(appRequire.resolve('@volter/world-core/runtime'), '@volter/world-core');
130
- for (const [name, identity] of [[metadata.name, candidate], ['@volter/world-core', kernel]] as const) {
174
+ const peerVersions = consumerPeerVersions(metadata, cliEntry, sdkDependencies);
175
+ const entries = consumerEntries(appRoot, [...Object.keys(peerVersions), ...Object.keys(sdkDependencies)]);
176
+ const peers = Object.keys(peerVersions).map((name) => [name, packageIdentity(entries.get(name)!, name)] as const);
177
+ for (const [name, identity] of [[metadata.name, candidate], ['@volter/world-core', kernel], ...peers] as const) {
131
178
  const location = relative(join(appRoot, 'node_modules'), identity.directory);
132
179
  if (location.startsWith('..') || isAbsolute(location)) throw new Error(`${name}: consumer resolves an evaluator or ancestor dependency; install it in the app`);
133
180
  }
134
181
  const appLock = dependencyLock(appRoot), appLockBytes = appLock.bytes;
135
182
  if (candidate.version !== metadata.version || appLock.entry(metadata.name)?.integrity !== options.artifactIntegrity) throw new Error('Consumer app candidate does not match the exact assessed artifact');
136
183
  if (kernel.version !== tools['@volter/world-core'] || appLock.entry('@volter/world-core')?.version !== kernel.version) throw new Error('Consumer app kernel does not match the CLI kernel pin');
137
- 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 };
138
- const facts = JSON.parse(readFileSync(join(packDir, 'generated/pack-facts.json'), 'utf8'));
139
- const vendor = Object.keys(facts.packs ?? {})[0];
140
- if (!vendor || Object.keys(facts.packs).length !== 1) throw new Error('Consumer workflow requires one selected artifact identity');
141
- const recipe = consumerRecipe(vendor, packDir);
184
+ for (const [name, identity] of peers) if (identity.version !== peerVersions[name] || appLock.entry(name)?.version !== identity.version) throw new Error(`${name}: consumer peer does not match the installed CLI closure and exact application lock version ${peerVersions[name]}`);
185
+ const sdks = Object.entries(sdkDependencies).map(([name, version]) => {
186
+ const entry = realpathSync(entries.get(name)!), identity = packageIdentity(entry, name);
187
+ const location = relative(join(appRoot, 'node_modules'), identity.directory);
188
+ if (location.startsWith('..') || isAbsolute(location)) throw new Error(`${name}: customer SDK resolves outside the application installation`);
189
+ if (identity.version !== version || appLock.entry(name)?.version !== version) throw new Error(`${name}: customer SDK does not match its exact recipe and application lock version ${version}`);
190
+ return { package: name, version, entry: relative(appRoot, entry).split('\\').join('/') };
191
+ });
192
+ 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 };
142
193
  const commands: Array<{ label: string; argv: string[]; exitCode: number | null; stdout: string; stderr: string }> = [];
143
194
  const failures: string[] = [];
144
195
  const compiledCli = /\.[cm]?js$/.test(cliEntry);
145
196
  let nodeVersion: string | null = null;
146
197
  const execute = async (label: string, executable: string, argv: string[]) => {
147
- const result = await new Promise<{ exitCode: number | null; stdout: string; stderr: string }>((done) => {
148
- const child = spawn(executable, argv, { cwd: options.appDir, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
149
- let stdout = '', stderr = '';
150
- child.stdout.on('data', (chunk) => { stdout += chunk; });
151
- child.stderr.on('data', (chunk) => { stderr += chunk; });
152
- child.on('error', (error) => done({ exitCode: 1, stdout, stderr: `${stderr}\n${error.message}` }));
153
- child.on('close', (exitCode) => done({ exitCode, stdout, stderr }));
154
- });
198
+ const result = await runConsumerCommand([executable, ...argv], options.appDir, process.env);
155
199
  commands.push({ label, argv: [executable, ...argv], ...result });
156
200
  if (result.exitCode !== 0) throw new Error(`${label} exited ${result.exitCode}: ${result.stderr.trim().split('\n')[0] ?? ''}`);
157
201
  return result.stdout;
@@ -213,7 +257,7 @@ export async function runConsumerWorkflow(packDir: string, options: ConsumerOpti
213
257
  }
214
258
  }
215
259
  return {
216
- measured: true, passed: failures.length === 0 && teardownVerified,
260
+ measured: true as const, passed: failures.length === 0 && teardownVerified,
217
261
  scope: 'Exact installed artifact; normal CLI initialization, unchanged SDK, result assertions and retained-state read-only resume where applicable',
218
262
  package: metadata.name, version: metadata.version, integrity: options.artifactIntegrity,
219
263
  tools, application,
@@ -227,3 +271,21 @@ export async function runConsumerWorkflow(packDir: string, options: ConsumerOpti
227
271
  at: new Date().toISOString(),
228
272
  };
229
273
  }
274
+
275
+ /** The same installed execution can prove a declared SDK when the standard has no built-in client module. */
276
+ export function consumerClientProof(packDir: string, vendor: string, sdks: string[], result?: ConsumerResult): { failures: string[]; note?: string } {
277
+ if (!existsSync(join(packDir, 'journeys/first-use.json'))) return { failures: ['the pack supplies no journeys/first-use.json installed SDK workflow'] };
278
+ const recipe = consumerRecipe(vendor, packDir);
279
+ const dependencies = Object.entries(recipeDependencies(recipe)).filter(([name]) => sdks.includes(name));
280
+ if (!dependencies.length) return { failures: ['the installed workflow has no exact direct dependency named by adoption.sdks'] };
281
+ if (!result?.measured || !result.passed || result.failures.length || !result.teardownVerified) return { failures: ['the installed SDK workflow has no passing execution with verified teardown'] };
282
+ const metadata = JSON.parse(readFileSync(join(packDir, 'package.json'), 'utf8'));
283
+ const candidate = result.application.candidate;
284
+ 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) return { failures: ['the installed SDK result does not identify this exact app-local candidate artifact'] };
285
+ if (result.fixture !== 'journeys/first-use.json' || result.sourceInputsSha256 !== consumerSourceDigest(packDir) || result.recipeSha256 !== createHash('sha256').update(JSON.stringify(recipe)).digest('hex')) return { failures: ['the installed SDK result does not describe the current behavior inputs and authored recipe'] };
286
+ for (const [name, version] of dependencies) {
287
+ const sdk = result.application.sdks.find((entry) => entry.package === name);
288
+ if (sdk?.version !== version || !sdk.entry.startsWith('node_modules/') || sdk.entry.split('/').includes('..')) return { failures: [`${name}: the installed SDK result has no exact app-local ${version} resolution`] };
289
+ }
290
+ return { failures: [], note: `installed customer workflow through ${dependencies.map(([name, version]) => `${name}@${version}`).join(', ')}; exact artifact and current recipe` };
291
+ }
@@ -2,8 +2,9 @@
2
2
  // create-pack — the files of a Protocol 3 pack that nobody should write (the board's `create-pack` card;
3
3
  // docs/contributing/architecture.md, "Creating a pack"), and the brief an author writes the rest from.
4
4
  //
5
- // bun scripts/create-pack.ts <dir> --init <vendor> write every fixed file of a new pack that does not exist yet
6
- // bun scripts/create-pack.ts <dir> --init <vendor> --lanes a,b
5
+ // twin-standard create <dir> --init <vendor> --package <@scope/name>
6
+ // write every fixed file of a new pack that does not exist yet
7
+ // twin-standard create <dir> --init <vendor> --package <@scope/name> --lanes a,b
7
8
  // the same for a pack of lanes, one per API of the vendor, each
8
9
  // derived from its own spec (<dir>/<lane>/spec): each lane's
9
10
  // manifest, fetch, index and semantics, and the pack's server
@@ -12,11 +13,11 @@
12
13
  // index}.ts, src/semantics/{states,shared,doors}.ts,
13
14
  // journeys/{demand,first-use}.json): the author fills the manifest's
14
15
  // vendor facts, the descriptor's hosts and the families
15
- // bun scripts/create-pack.ts <unit> --index write src/semantics/index.ts, the handler map, and src/fetch.ts from the
16
+ // twin-standard create <unit> --index write src/semantics/index.ts, the handler map, and src/fetch.ts from the
16
17
  // families' files (every exported function, by its name)
17
- // bun scripts/create-pack.ts <unit> --index --building the same during construction; unfinished operations stay gaps,
18
+ // twin-standard create <unit> --index --building the same during construction; unfinished operations stay gaps,
18
19
  // and implemented operations must match their decisions
19
- // bun scripts/create-pack.ts <unit> --brief [--out f] the author's brief: every operation the derived core does not
20
+ // twin-standard create <unit> --brief [--out f] the author's brief: every operation the derived core does not
20
21
  // serve and no handler answers yet, by family, with what the
21
22
  // vendored spec says of it (summary, description, parameters,
22
23
  // the answers it documents and their examples), so a handler
@@ -69,7 +70,7 @@ export function handlerIndex(dir: string): string {
69
70
  const ids = existsSync(surfacePath) ? ((parseExactJson(readFileSync(surfacePath, 'utf8')) as { operations?: Array<{ id: string }> }).operations ?? []).map((o) => o.id) : [];
70
71
  // an index written before the derive has no operations to map, and every served call would fall to the gap (404): refuse
71
72
  // it rather than write an empty map (2026-09-30: six packs committed so)
72
- if (!existsSync(surfacePath) && fams.length) throw new Error(`${dir}: no src/generated/surface.gen.json yet; derive the unit (scripts/derive-pack.ts) before --index`);
73
+ if (!existsSync(surfacePath) && fams.length) throw new Error(`${dir}: no src/generated/surface.gen.json yet; derive the unit (twin-standard derive) before --index`);
73
74
  const where = new Map(fams.flatMap((f) => f.handlers.map((h) => [h, f.family] as const)));
74
75
  const named = new Set([...new Set(ids)].map(handlerName));
75
76
  const stray = [...where.keys()].filter((h) => !named.has(h));
@@ -269,7 +270,7 @@ export async function brief(unit: string): Promise<string> {
269
270
  const s = spec.get(o.id);
270
271
  lines.push(`### ${o.id}${unmodeled.has(o.id) ? ' (unmodeled)' : ''}`, '', `\`${o.method.toUpperCase()} ${o.path}\` — ${o.class}${s?.summary ? `: ${oneLine(s.summary)}` : ''}`);
271
272
  // the citation a handler carries, ready to paste: by operationId where the spec names one, else by its JSON pointer,
272
- // quoting the spec's own words (derive-pack refuses a quote the spec does not hold)
273
+ // quoting the spec's own words (the derivation refuses a quote the spec does not hold)
273
274
  const words = oneLine(s?.summary ?? s?.description ?? '').split(/(?<=\.)\s/)[0]!.replace(/"/g, '\\"');
274
275
  const cite = s && (s as { operationId?: string }).operationId === o.id ? `spec:${o.id}` : `spec:/paths/${o.path.replace(/~/g, '~0').replace(/\//g, '~1')}/${o.method.toLowerCase()}`;
275
276
  if (words) lines.push('', `Cite: \`// source: ${cite} "${words}"\``);
@@ -331,7 +332,7 @@ export function packIndexFile(dir: string, vendor: string): string {
331
332
 
332
333
  /** A pack's `src/server.ts` (a fixed file), the kernel's serve seam around its fetch, naming only what its unit has so
333
334
  * it typechecks and a bundler follows every import: the plain seam (serveHttp, which takes the sockets a fetch carries
334
- * itself: "Other wires"), or, for a unit whose spec is a proto, the gRPC seam over the schema derive-pack generated
335
+ * itself: "Other wires"), or, for a unit whose spec is a proto, the gRPC seam over the schema the derivation generated
335
336
  * (src/generated/proto.gen.json, imported by its literal path). `--init` writes the plain one, `--index` the gRPC one once
336
337
  * the unit's proto is derived. */
337
338
  export function serverFile(vendor: string, grpc: boolean, wire = false, live = false): string {
@@ -436,7 +437,7 @@ export function initFiles(vendor: string): Record<string, string> {
436
437
  'src/semantics/states.ts': `// ${vendor}'s state machines and state rulings, each transition cited (architecture, "State").\nexport const states = {};\n`,
437
438
  '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`,
438
439
  '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`,
439
- '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-pack). 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`,
440
+ '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`,
440
441
  'src/server.ts': serverFile(vendor, false),
441
442
  'src/cli.ts': cliFile(vendor),
442
443
  '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`,
@@ -515,7 +516,7 @@ export async function fixedFiles(dir: string): Promise<Record<string, string>> {
515
516
  if (!vendor || !/^[a-z][a-z0-9-]*$/.test(vendor)) throw new Error('Pack manifest must declare its vendor identity independently of the npm package name');
516
517
  files['src/index.ts'] = packIndexFile(dir, vendor);
517
518
  if (!existsSync(join(dir, 'src', 'stream.ts'))) {
518
- // a unit whose spec is a proto (derive-pack generated its schema) is served over gRPC: its serve seam in that form
519
+ // a unit whose spec is a proto (the derivation generated its schema) is served over gRPC: its serve seam in that form
519
520
  const lanesOnly = !hasOwnSurface(dir) && readdirSync(dir).some((n) => existsSync(join(dir, n, 'src', 'manifest.ts')));
520
521
  const grpc = !lanesOnly && existsSync(join(dir, 'src', 'generated', 'proto.gen.json'));
521
522
  // a unit that takes broadcasts over RTMP (its manifest's `live`) has the live forms --index writes, so they reproduce
@@ -529,7 +530,7 @@ export async function fixedFiles(dir: string): Promise<Record<string, string>> {
529
530
  export async function createPack(args: string[]): Promise<void> {
530
531
  const [unit, ...rest] = args;
531
532
  if (!unit || (!rest.includes('--index') && !rest.includes('--brief') && !rest.includes('--init'))) {
532
- console.error('usage: bun scripts/create-pack.ts <dir> --init <vendor> | <unit> --index [--building] | --brief [--out <file>]');
533
+ console.error('usage: twin-standard create <dir> --init <vendor> --package <@scope/name> | <unit> --index [--building] | --brief [--out <file>]');
533
534
  process.exit(2);
534
535
  }
535
536
  if (rest.includes('--building') && (!rest.includes('--index') || rest.includes('--init') || rest.includes('--brief'))) {
@@ -598,7 +599,7 @@ export async function createPack(args: string[]): Promise<void> {
598
599
  console.log(`create-pack: wrote ${join(dir, 'src', 'index.ts')}`);
599
600
  // what the grade's typecheck compiles: a pack made before init wrote it gets it here
600
601
  if (!existsSync(join(dir, 'tsconfig.json'))) { writeFileSync(join(dir, 'tsconfig.json'), TSCONFIG); console.log(`create-pack: wrote ${join(dir, 'tsconfig.json')}`); }
601
- // a unit whose spec is a proto (derive-pack generated its schema) is served over gRPC: its serve seam in that form,
602
+ // a unit whose spec is a proto (the derivation generated its schema) is served over gRPC: its serve seam in that form,
602
603
  // 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)
603
604
  if (!lanesOnly && existsSync(join(dir, 'src', 'generated', 'proto.gen.json'))) {
604
605
  const wire = existsSync(join(dir, 'src', 'stream.ts'));
@@ -0,0 +1,38 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import ts from 'typescript';
4
+
5
+ /** A literal property's effective value; a later spread or computed key cannot establish an import permission. */
6
+ function property(expression: ts.Expression | undefined, name: string): ts.Expression | undefined {
7
+ while (expression && (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression))) expression = expression.expression;
8
+ if (!expression || !ts.isObjectLiteralExpression(expression)) return undefined;
9
+ for (const member of [...expression.properties].reverse()) {
10
+ if (!ts.isPropertyAssignment(member) || ts.isComputedPropertyName(member.name)) return undefined;
11
+ if (member.name.text === name) return member.initializer;
12
+ }
13
+ return undefined;
14
+ }
15
+
16
+ /** The declared credential issuer's public subpaths, pinned as a direct runtime dependency. Reading the manifest as
17
+ * data keeps the source/tarball gate independent of dependency installation; typechecking verifies exported paths. */
18
+ export function credentialIssuerImports(unitDir: string, packageDir: string): (specifier: string) => boolean {
19
+ const path = join(unitDir, 'src', 'manifest.ts');
20
+ const packagePath = join(packageDir, 'package.json');
21
+ if (!existsSync(path) || !existsSync(packagePath)) return () => false;
22
+ const source = ts.createSourceFile(path, readFileSync(path, 'utf8'), ts.ScriptTarget.Latest, true);
23
+ const manifests = source.statements.flatMap(statement => ts.isVariableStatement(statement)
24
+ && statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)
25
+ ? statement.declarationList.declarations.filter(declaration => ts.isIdentifier(declaration.name) && declaration.name.text === 'manifest') : []);
26
+ const issuer = manifests.length === 1 ? property(property(manifests[0]!.initializer, 'descriptor'), 'credentialIssuer') : undefined;
27
+ if (!issuer || !ts.isStringLiteral(issuer) || !/^[a-z]/.test(issuer.text) || /[^a-z0-9-]/.test(issuer.text)) return () => false;
28
+ const packageName = `@volter/twin-${issuer.text}`;
29
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf8')) as { name?: string; dependencies?: Record<string, string> };
30
+ const version = pkg.dependencies?.[packageName];
31
+ const number = '(?:0|[1-9][0-9]*)';
32
+ const prerelease = '(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)';
33
+ const exactVersion = new RegExp(`^${number}\\.${number}\\.${number}(?:-${prerelease}(?:\\.${prerelease})*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$`);
34
+ if (pkg.name === packageName || typeof version !== 'string' || exactVersion.exec(version)?.[0] !== version) return () => false;
35
+ const prefix = `${packageName}/`;
36
+ return specifier => specifier.startsWith(prefix) && specifier.slice(prefix.length).split('/').every(segment =>
37
+ segment !== '' && segment !== '.' && segment !== '..' && !/[^A-Za-z0-9._-]/.test(segment) && segment.toLowerCase() !== 'node_modules');
38
+ }
package/src/derive.ts CHANGED
@@ -5,7 +5,7 @@ import { parseExactJson, exactInteger } from '@volter/world-core/exact-json';
5
5
  // vendored spec (`<pack>/spec/openapi.json[.gz]`), builds the spec IR (./spec-ir.ts) and writes the pack's
6
6
  // `src/generated/`. Deterministic: a second run over the same spec writes the same bytes. Never hand-edit what it writes.
7
7
  //
8
- // Usage: twin-standard derive <pack dir> [--out <dir>] (twin-world: bun scripts/derive-pack.ts <vendor>)
8
+ // Usage: twin-standard derive <pack dir> [--out <dir>]
9
9
 
10
10
  import { parse as parseYaml } from 'yaml';
11
11
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
@@ -41,7 +41,7 @@ const PKG = unitAt(vendor).dir;
41
41
  const rfc = existsSync(join(PKG, 'spec')) ? readdirSync(join(PKG, 'spec')).find((f) => /^rfc\d+\.txt$/.test(f)) : undefined;
42
42
  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));
43
43
  if (!specPath) {
44
- console.error(`derive-pack: no spec at ${join(PKG, 'spec')}/openapi.{json,yaml}[.gz], smithy.json[.gz], rfcNNNN.txt, proto/, commands/, command-builders.json or client-ops.json`);
44
+ 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`);
45
45
  process.exit(SOURCE_REFUSAL);
46
46
  }
47
47
  const rpcService = basename(specPath) === 'proto';
@@ -308,11 +308,11 @@ if (existsSync(manifestPath)) {
308
308
  }
309
309
  // a resource's refresh scope ("The real-system adapters", What a pack declares) names the list that enumerates it or
310
310
  // its retrieve, or says why the vendor offers no read-back; whether every stored resource declares one is
311
- // pack-standing's report, not this build's
311
+ // not this build's to decide
312
312
  // (a resource the spec names is read by its own list or retrieve; one the manifest names apart from the spec's, an RPC
313
313
  // vendor's, by the list or read the scope names, with `items` where the answer holds them when the spec does not say)
314
314
  const specNamed = new Set(ir.resources.map((r) => r.name));
315
- const scopes = Object.entries(manifest.resources as Record<string, { storedAs?: string; refresh?: { list?: string; get?: string; none?: string; items?: string; detail?: string } }>).flatMap(([name, decl]) => {
315
+ const scopes = Object.entries(manifest.resources as Record<string, { storedAs?: string; refresh?: { list?: string; get?: string; known?: boolean; none?: string; items?: string; detail?: string } }>).flatMap(([name, decl]) => {
316
316
  const r = decl.refresh;
317
317
  if (!r) return [];
318
318
  if ('graphql' in r && r.graphql) {
@@ -363,6 +363,8 @@ if (existsSync(manifestPath)) {
363
363
  if (r.get !== undefined) {
364
364
  const o = op(r.get);
365
365
  if (o?.class === 'retrieve' && ours(o)) return [];
366
+ // A known subject may be addressed by an id query whose native answer is a list envelope.
367
+ if (r.known && o?.class === 'list' && o.answers?.list && ours(o) && o.method.toUpperCase() === 'GET') return [];
366
368
  // a resource the spec does not name (a client-derived spec classes every request an action) is read by the GET
367
369
  // the scope names, the answer itself the subject
368
370
  if (o && !specNamed.has(name) && !o.resource && String(o.method).toUpperCase() === 'GET') return [];
package/src/gate.ts CHANGED
@@ -19,6 +19,7 @@ import ts from 'typescript';
19
19
  import { grade, type GradeOptions } from './grade.ts';
20
20
  import { withCheckoutFiles } from './protocol-3.ts';
21
21
  import { isPackArtifact } from './pack-files.ts';
22
+ import { credentialIssuerImports } from './credential-issuer-import.ts';
22
23
 
23
24
  /** Every unit of a pack repository: each pack (a directory with a package.json) and each lane in it (a directory with
24
25
  * its own src/manifest.ts and no package.json). */
@@ -102,6 +103,7 @@ export function laneRootFailures(dir: string): string[] {
102
103
  if (!/\bcreateVendorFetch\(/.test(src('fetch.ts'))) failures.push('layout: src/fetch.ts builds no createVendorFetch: the kernel routes a vendor\'s lanes from its manifest, nothing by hand');
103
104
  // the root's own files hold to the pack contract as a unit's do (the grade's kernel-contract)
104
105
  const beyond: string[] = [];
106
+ const issuerImport = credentialIssuerImports(dir, dir);
105
107
  for (const n of existsSync(join(dir, 'src')) ? readdirSync(join(dir, 'src')).filter((x) => /\.tsx?$/.test(x) && !x.endsWith('.d.ts')) : []) {
106
108
  // the file's imports as TypeScript reads them, as the grade's kernel-contract does
107
109
  for (const from of ts.preProcessFile(readFileSync(join(dir, 'src', n), 'utf8'), true, true).importedFiles.map((i) => i.fileName)) {
@@ -111,6 +113,7 @@ export function laneRootFailures(dir: string): string[] {
111
113
  if (/^\.\.\/[^/]+\/src\//.test(from) && n !== 'fetch.ts' && !laneData) { beyond.push(`src/${n} imports ${from}: only src/fetch.ts reaches a lane`); continue; }
112
114
  if (n === 'manifest.ts' && from !== '@volter/world-core') { beyond.push(`src/manifest.ts imports ${from}: the manifest is data`); continue; }
113
115
  if (from.startsWith('.') || from === '@volter/world-core' || from === '@volter/world-ui') continue;
116
+ if (issuerImport(from)) continue;
114
117
  if (n === 'cli.ts' && (from === '@volter/world-core/args' || from === '@volter/world-core/lifecycle')) continue;
115
118
  beyond.push(`src/${n} imports ${from}`);
116
119
  }
package/src/index.ts 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, type CriterionGrade, type GradeOptions, type StandardGrade } from './grade.ts';
7
- export { conformance, decisionTableProblems, type ConformanceResult, UNIT_FILES, unitFilesMissing } from './conformance.ts';
7
+ export { conformance, decisionTableProblems, type ConformanceResult, UNIT_FILES } from './conformance.ts';
8
8
  export { laneRootFailures, p3Gate, unitsOf } from './gate.ts';
9
9
  export { conformPublished, gatePublished, vendorOfPackage, type PublishedGate } from './published.ts';
10
10
  export { handlerName, protocol3, PLANNED, withCheckoutFiles } from './protocol-3.ts';
package/src/lanes.ts CHANGED
@@ -4,6 +4,13 @@
4
4
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
 
7
+ /** The pack and its manifest-bearing lanes, without crossing another package's boundary. */
8
+ export function unitsIn(pack: string): string[] {
9
+ if (!existsSync(pack)) 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
+ }
13
+
7
14
  /** Metadata such as sources.json is not a derivable API of a vendor whose APIs all belong to its lanes. */
8
15
  export function hasOwnSurface(pkg: string): boolean {
9
16
  if (existsSync(join(pkg, 'src', 'generated', 'surface.gen.json'))) return true;
package/src/pack-facts.ts CHANGED
@@ -8,7 +8,7 @@ export type PackFacts = {
8
8
  serverBacking?: { command: string };
9
9
  maxRequestBodySize?: number;
10
10
  adoption?: { sdks?: string[]; pypi?: string[]; scopes?: string[]; envStems?: string[]; worldIds?: string[]; tools?: Array<{ package: string; usage: 'build' | 'deployment' }>; configFiles?: Array<{ file: string; usage: 'application' | 'build' | 'deployment'; when?: string }> };
11
- hosts?: Array<{ host?: string; suffix?: string; hostPattern?: string; pathPattern?: string; demandPathPattern?: string | false; key?: string; exclude?: true }>;
11
+ hosts?: Array<{ host?: string; suffix?: string; hostPattern?: string; pathPattern?: string; demandPathPattern?: string | false; key?: string; exclude?: true; basic?: string }>;
12
12
  hostsNone?: string;
13
13
  hostsClaimed?: { door: string; note: string };
14
14
  /** The subject types the twin projects (packRegistry `resources`) — invariant R2 at the resource
@@ -42,6 +42,8 @@ export type PackFacts = {
42
42
  managedDatabase?: { kind: 'postgres'; arg: string; user: string; database: string; clock: 'wall'; credentialField: string; note: string };
43
43
  /** Listeners the twin opens beside its main port (see TwinPack.sidePorts). */
44
44
  sidePorts?: Array<{ name: string; option: string; flag: string; path?: string; scheme?: string; note: string }>;
45
+ /** The vendor's own requests a World's generated seed makes (see TwinPack.seed). */
46
+ seed?: TwinPack['seed'];
45
47
  /** DERIVED, never declared: the pack's `create<Name>TwinServer` factory export — the
46
48
  * `({port,root,readOnly}) => {port,stop}` contract the colocated host mounts
47
49
  * (world-runtime/src/host.ts). Read off the module's own exports because the name is not
@@ -78,6 +80,7 @@ function validateHostRules(vendor: string, rules: NonNullable<PackFacts['hosts']
78
80
  try { new RegExp(rule.demandPathPattern); } catch (e) { throw new Error(`pack-facts: ${vendor} demandPathPattern does not compile: ${(e as Error).message}`); }
79
81
  }
80
82
  if (rule.exclude !== undefined && rule.exclude !== true) throw new Error(`pack-facts: ${vendor} host rule ${JSON.stringify(rule)} — exclude is \`true\` or absent, never a falsy flag`);
83
+ if (rule.basic !== undefined && (typeof rule.basic !== 'string' || !/^[^:\s]+$/.test(rule.basic) || rule.exclude)) throw new Error(`pack-facts: ${vendor} host rule ${JSON.stringify(rule)} — basic names a user (no colon, no space) on an included host`);
81
84
  const key = rule.key ?? vendor;
82
85
  // A foreign key is a WORLD identity too: `volter-world init` emits <KEY>_TWIN_URL only for the
83
86
  // ids in adoption.worldIds (covers.ts injectorVendorKeysFor), so a key not listed there would
@@ -221,6 +224,26 @@ export async function compilePackFacts(dirs: Array<{ vendor: string; dir: string
221
224
  }
222
225
  facts.sidePorts = pack.sidePorts;
223
226
  }
227
+ if (pack.seed !== undefined) {
228
+ // 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
229
+ // is a `/_twin/` stand-in
230
+ const seed = pack.seed as Record<string, unknown>;
231
+ const door = (value: unknown, what: string, standIn = false): void => {
232
+ let url: URL;
233
+ try { url = new URL(String(value)); } catch { throw new Error(`pack-facts: ${vendor} seed ${what} "${String(value)}" is not a URL`); }
234
+ if (url.protocol !== 'https:') throw new Error(`pack-facts: ${vendor} seed ${what} is the vendor's own https request`);
235
+ if (!standIn && url.pathname.startsWith('/_twin/')) throw new Error(`pack-facts: ${vendor} seed ${what} is the vendor's own request, never a /_twin/ door`);
236
+ };
237
+ if (!seed || typeof seed !== 'object' || Array.isArray(seed) || Object.keys(seed).some((k) => k !== 'signUp' && k !== 'domain')) throw new Error(`pack-facts: ${vendor} seed is { signUp?, domain? }`);
238
+ if (seed.signUp !== undefined) door(seed.signUp, 'signUp');
239
+ if (seed.domain !== undefined) {
240
+ const domain = seed.domain as Record<string, unknown>;
241
+ if (!domain || typeof domain !== 'object' || Array.isArray(domain) || Object.keys(domain).sort().join() !== 'add,record,settles,verify') throw new Error(`pack-facts: ${vendor} seed.domain is { add, record, verify, settles }`);
242
+ door(domain.add, 'domain.add'); door(domain.verify, 'domain.verify'); door(domain.record, 'domain.record', true);
243
+ if (typeof domain.settles !== 'string' || !/^\d+[smhd]$/.test(domain.settles)) throw new Error(`pack-facts: ${vendor} seed.domain.settles is a World-clock time (10m)`);
244
+ }
245
+ facts.seed = pack.seed;
246
+ }
224
247
  // The serve factory is DERIVED from the module's exports (see PackFacts.serveExport);
225
248
  // a pack exporting SEVERAL factories must declare which one `cli.ts serve` boots — the
226
249
  // exports alone cannot reveal that judgment (linear defaults to its DERIVED server).
@@ -240,7 +263,7 @@ export async function compilePackFacts(dirs: Array<{ vendor: string; dir: string
240
263
  if (Object.keys(facts).length > 0) packs[vendor] = facts;
241
264
  }
242
265
  return {
243
- '//': 'GENERATED by scripts/pack-facts.ts — current pack descriptors and explicit world-runtime server backings; no prior-artifact entries are retained.',
266
+ '//': 'GENERATED by twin-standard facts from the pack descriptor; no prior-artifact entries are retained.',
244
267
  packs: sortObject(packs),
245
268
  undeclared,
246
269
  };
package/src/protocol-3.ts CHANGED
@@ -17,6 +17,7 @@ import ts from 'typescript';
17
17
  import { fileURLToPath } from 'node:url';
18
18
  import { sharingLanesIn } from './lanes.ts';
19
19
  import { isPackArtifact } from './pack-files.ts';
20
+ import { credentialIssuerImports } from './credential-issuer-import.ts';
20
21
  import type { Criterion, GradeUnit, Standard } from './types.ts';
21
22
 
22
23
  // A path relative to another with '/' between its parts on every platform: git lists files that way, and every rule
@@ -170,7 +171,7 @@ function noReport(unit: GradeUnit): string {
170
171
  /** An operation's family: the first segment of its path after a version (`/v1/organizations/{id}` is `organizations`), a
171
172
  * dotted RPC method's first part (`chat.postMessage` is `chat`), a line command's name (architecture, "Pack layout"). */
172
173
  function familyOf(path: string): string {
173
- // the interim rule ("What an author writes"), until derive-pack writes the spec's own grouping on each operation
174
+ // the interim rule ("What an author writes"), until the derivation writes the spec's own grouping on each operation
174
175
  const segs = path.split('/').filter((x) => x && !/^v?\d+(\.\d+)?$|^\d{4}-\d{2}-\d{2}$/.test(x) && !x.startsWith('{'));
175
176
  if (!segs.length) return 'operations'; // named in a header: every path is `/`
176
177
  const first = segs[0]!;
@@ -261,7 +262,7 @@ const criteria: Criterion[] = [
261
262
  },
262
263
  {
263
264
  id: 'derived-reproducible', section: 'form',
264
- asks: 'derive-pack regenerates src/generated/ byte for byte',
265
+ asks: 'twin-standard derive regenerates src/generated/ byte for byte',
265
266
  check(unit) {
266
267
  const gen = join(unit.dir, 'src', 'generated');
267
268
  if (!existsSync(gen)) return fail('src/generated/ is missing');
@@ -278,9 +279,9 @@ const criteria: Criterion[] = [
278
279
  // the pack's own path and the scratch directory are this checkout's, not the pack's: the verdict names neither, so
279
280
  // a standing written from one checkout reads the same from any other
280
281
  if (run.status === null || run.signal !== null || run.error) return { pass: false, cacheable: false,
281
- reason: `derive-pack interrupted: status=${run.status} signal=${run.signal} error=${run.error?.message ?? ''}` };
282
- if (run.status !== 0) return { ...fail(`derive-pack refused: ${String(run.stderr || run.stdout).split(unit.dir).join('.').split(tmp).join('<out>').trim().split('\n').slice(-2).join(' ').slice(0, 200)}`), cacheable: run.status === 2 };
283
- return changed.length ? fail(`derive-pack writes differently: ${changed.slice(0, 5).join(', ')}${changed.length > 5 ? ` and ${changed.length - 5} more` : ''}`) : pass(`${filesUnder(gen).length} files reproduced`);
282
+ reason: `derive interrupted: status=${run.status} signal=${run.signal} error=${run.error?.message ?? ''}` };
283
+ if (run.status !== 0) return { ...fail(`derive refused: ${String(run.stderr || run.stdout).split(unit.dir).join('.').split(tmp).join('<out>').trim().split('\n').slice(-2).join(' ').slice(0, 200)}`), cacheable: run.status === 2 };
284
+ return changed.length ? fail(`derive writes differently: ${changed.slice(0, 5).join(', ')}${changed.length > 5 ? ` and ${changed.length - 5} more` : ''}`) : pass(`${filesUnder(gen).length} files reproduced`);
284
285
  },
285
286
  },
286
287
  {
@@ -340,13 +341,14 @@ const criteria: Criterion[] = [
340
341
  },
341
342
  {
342
343
  id: 'handler-contract', section: 'form',
343
- 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',
344
+ 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',
344
345
  check(unit) {
345
346
  const dir = join(unit.dir, 'src', 'semantics');
346
347
  if (!existsSync(dir)) return pass('no handlers');
347
348
  const ops = operationsOf(unit);
348
349
  const families = new Set(ops.values());
349
350
  const found: string[] = [];
351
+ const issuerImport = credentialIssuerImports(unit.dir, packageOf(unit));
350
352
  for (const f of filesUnder(dir).filter((x) => /\.tsx?$/.test(x) && !TEST_FILE.test(x))) {
351
353
  const text = stripComments(readFileSync(join(dir, f), 'utf8'));
352
354
  const raw = readFileSync(join(dir, f), 'utf8');
@@ -363,7 +365,7 @@ const criteria: Criterion[] = [
363
365
  const engineModule = /^\.\.\/engine\/[A-Za-z0-9_-]+\.ts$/.test(from) || vendorPath(unit, from, 3, /^engine\/[A-Za-z0-9_-]+\.ts$/);
364
366
  // a lane over its vendor's state takes the vendor's states and shared helpers ("Pack layout")
365
367
  const vendorShared = vendorPath(unit, from, 3, /^(semantics\/(states|shared)|manifest)\.ts$/);
366
- if (from !== '@volter/world-core' && !/^\.\/(states|shared)\.ts$/.test(from) && !mapImport && !engineModule && !vendorShared) found.push(`${f} imports ${from}`);
368
+ if (from !== '@volter/world-core' && !/^\.\/(states|shared)\.ts$/.test(from) && !mapImport && !engineModule && !vendorShared && !issuerImport(from)) found.push(`${f} imports ${from}`);
367
369
  }
368
370
  // the handler map is typed over the narrow context, so the compiler holds every handler it names to the contract
369
371
  if (f === 'index.ts' && !/:\s*Record<\s*string\s*,\s*Handler\s*>/.test(text)) found.push('index.ts does not type its handler map Record<string, Handler>');
@@ -468,11 +470,12 @@ const criteria: Criterion[] = [
468
470
  },
469
471
  {
470
472
  id: 'kernel-contract', section: 'form',
471
- 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",
473
+ 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",
472
474
  check(unit) {
473
475
  const src = join(unit.dir, 'src');
474
476
  if (!existsSync(src)) return fail('src/ is missing');
475
477
  const found: string[] = [];
478
+ const issuerImport = credentialIssuerImports(unit.dir, packageOf(unit));
476
479
  for (const f of filesUnder(src).filter((x) => /\.tsx?$/.test(x) && !TEST_FILE.test(x) && !x.startsWith('generated/'))) {
477
480
  // the file's imports, re-exports and dynamic imports as TypeScript reads them (a string that only reads like one,
478
481
  // an error message's "from '<name>'", is none)
@@ -512,6 +515,7 @@ const criteria: Criterion[] = [
512
515
  for (const from of specifiers) {
513
516
  if (from.startsWith('.')) continue;
514
517
  if (from === '@volter/world-core' || from === '@volter/world-ui') continue;
518
+ if (issuerImport(from)) continue;
515
519
  if (f === 'cli.ts' && (from === '@volter/world-core/args' || from === '@volter/world-core/lifecycle')) continue;
516
520
  found.push(`${f} imports ${from}`);
517
521
  }