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