@volter/world-runtime 3.0.162 → 3.0.164

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 (140) hide show
  1. package/dist/src/ancestry.js +2 -2
  2. package/dist/src/app-url.d.ts +5 -1
  3. package/dist/src/app-url.js +14 -1
  4. package/dist/src/branch.js +28 -8
  5. package/dist/src/browser-files.d.ts +5 -0
  6. package/dist/src/browser-files.js +14 -0
  7. package/dist/src/browsers.d.ts +2 -3
  8. package/dist/src/browsers.js +4 -4
  9. package/dist/src/catalog.d.ts +2 -1
  10. package/dist/src/catalog.js +3 -2
  11. package/dist/src/changeset.js +1 -1
  12. package/dist/src/cli-commands.js +71 -18
  13. package/dist/src/cli-load-clock.d.ts +1 -0
  14. package/dist/src/cli-load-clock.js +2 -0
  15. package/dist/src/cli.js +5 -1
  16. package/dist/src/clickhouse-backing.d.ts +2 -0
  17. package/dist/src/clickhouse-backing.js +145 -0
  18. package/dist/src/command-lifetime.d.mts +1 -1
  19. package/dist/src/command-lifetime.mjs +5 -2
  20. package/dist/src/configs.js +1 -1
  21. package/dist/src/consumers.js +1 -1
  22. package/dist/src/covers.d.ts +4 -2
  23. package/dist/src/covers.js +8 -4
  24. package/dist/src/go-mysql-binding.d.ts +19 -0
  25. package/dist/src/go-mysql-binding.js +51 -0
  26. package/dist/src/guest-processes.d.mts +1 -1
  27. package/dist/src/guest-processes.mjs +22 -14
  28. package/dist/src/host-cli.js +8 -1
  29. package/dist/src/host-ready.d.ts +16 -2
  30. package/dist/src/host-ready.js +66 -39
  31. package/dist/src/host-worker.js +7 -1
  32. package/dist/src/host.d.ts +26 -0
  33. package/dist/src/host.js +93 -3
  34. package/dist/src/infra-definition.d.ts +35 -0
  35. package/dist/src/infra-definition.js +146 -0
  36. package/dist/src/infra.d.ts +3 -0
  37. package/dist/src/infra.js +375 -105
  38. package/dist/src/init.d.ts +46 -0
  39. package/dist/src/init.js +445 -18
  40. package/dist/src/lifecycle-record.d.ts +5 -1
  41. package/dist/src/lifecycle-record.js +27 -4
  42. package/dist/src/local-branches.js +1 -1
  43. package/dist/src/mariadb-bootstrap.d.ts +32 -0
  44. package/dist/src/mariadb-bootstrap.js +90 -0
  45. package/dist/src/mariadb-supplier.d.ts +23 -0
  46. package/dist/src/mariadb-supplier.js +215 -0
  47. package/dist/src/mongodb-backing.d.ts +4 -0
  48. package/dist/src/mongodb-backing.js +71 -1
  49. package/dist/src/native-mariadb-backing.d.ts +13 -0
  50. package/dist/src/native-mariadb-backing.js +512 -0
  51. package/dist/src/native-postgres-backing.d.ts +4 -10
  52. package/dist/src/native-postgres-backing.js +428 -43
  53. package/dist/src/native-server-inputs.d.ts +16 -0
  54. package/dist/src/native-server-inputs.js +204 -0
  55. package/dist/src/origin.js +3 -3
  56. package/dist/src/original-inputs.d.ts +14 -0
  57. package/dist/src/original-inputs.js +84 -0
  58. package/dist/src/pack-facts.d.ts +9 -1
  59. package/dist/src/pglite-backing.d.ts +41 -17
  60. package/dist/src/pglite-backing.js +213 -79
  61. package/dist/src/pglite-host.mjs +482 -10
  62. package/dist/src/process-groups.d.mts +6 -1
  63. package/dist/src/process-groups.d.ts +2 -1
  64. package/dist/src/process-groups.js +1 -1
  65. package/dist/src/process-groups.mjs +82 -15
  66. package/dist/src/process-signal.d.mts +2 -2
  67. package/dist/src/process-signal.mjs +29 -9
  68. package/dist/src/project-inspect.d.ts +12 -0
  69. package/dist/src/project-inspect.js +41 -4
  70. package/dist/src/proxy-env.d.ts +34 -0
  71. package/dist/src/proxy-env.js +114 -0
  72. package/dist/src/redirect-proxy.d.ts +1 -31
  73. package/dist/src/redirect-proxy.js +3 -112
  74. package/dist/src/redis-backing.d.ts +6 -4
  75. package/dist/src/redis-backing.js +20 -168
  76. package/dist/src/runtime.d.ts +1 -0
  77. package/dist/src/runtime.js +503 -218
  78. package/dist/src/schema.d.ts +1 -0
  79. package/dist/src/schema.js +5 -3
  80. package/dist/src/served-world.js +15 -10
  81. package/dist/src/service-exit.d.ts +3 -2
  82. package/dist/src/service-exit.js +7 -5
  83. package/dist/src/twin-backing.d.ts +47 -0
  84. package/dist/src/twin-backing.js +343 -0
  85. package/dist/src/up-cli.js +56 -3
  86. package/package.json +4 -4
  87. package/src/ancestry.ts +2 -2
  88. package/src/app-url.ts +9 -2
  89. package/src/branch.ts +26 -8
  90. package/src/browser-files.ts +15 -0
  91. package/src/browsers.ts +4 -4
  92. package/src/catalog.ts +4 -3
  93. package/src/changeset.ts +1 -1
  94. package/src/cli-commands.ts +85 -18
  95. package/src/cli-load-clock.ts +2 -0
  96. package/src/cli.ts +5 -1
  97. package/src/clickhouse-backing.ts +70 -0
  98. package/src/command-lifetime.d.mts +1 -1
  99. package/src/command-lifetime.mjs +5 -2
  100. package/src/configs.ts +1 -1
  101. package/src/consumers.ts +1 -1
  102. package/src/covers.ts +9 -4
  103. package/src/go-mysql-binding.ts +49 -0
  104. package/src/guest-processes.d.mts +1 -1
  105. package/src/guest-processes.mjs +22 -14
  106. package/src/host-cli.ts +7 -1
  107. package/src/host-ready.ts +62 -31
  108. package/src/host-worker.ts +6 -2
  109. package/src/host.ts +88 -6
  110. package/src/infra-definition.ts +135 -0
  111. package/src/infra.ts +250 -45
  112. package/src/init.ts +416 -17
  113. package/src/lifecycle-record.ts +18 -4
  114. package/src/local-branches.ts +1 -1
  115. package/src/mariadb-bootstrap.ts +107 -0
  116. package/src/mariadb-supplier.ts +199 -0
  117. package/src/mongodb-backing.ts +59 -1
  118. package/src/native-mariadb-backing.ts +418 -0
  119. package/src/native-postgres-backing.ts +353 -41
  120. package/src/native-server-inputs.ts +169 -0
  121. package/src/origin.ts +3 -3
  122. package/src/original-inputs.ts +72 -0
  123. package/src/pack-facts.ts +1 -1
  124. package/src/pglite-backing.ts +192 -80
  125. package/src/pglite-host.mjs +482 -10
  126. package/src/process-groups.d.mts +6 -1
  127. package/src/process-groups.mjs +82 -15
  128. package/src/process-groups.ts +2 -1
  129. package/src/process-signal.d.mts +2 -2
  130. package/src/process-signal.mjs +29 -9
  131. package/src/project-inspect.ts +37 -4
  132. package/src/proxy-env.ts +128 -0
  133. package/src/redirect-proxy.ts +3 -125
  134. package/src/redis-backing.ts +19 -136
  135. package/src/runtime.ts +342 -107
  136. package/src/schema.ts +5 -4
  137. package/src/served-world.ts +14 -10
  138. package/src/service-exit.ts +5 -3
  139. package/src/twin-backing.ts +300 -0
  140. package/src/up-cli.ts +52 -3
@@ -1,26 +1,27 @@
1
+ import type { statusWorld } from './runtime.ts';
1
2
  // Every volter-world command but `up` (./up-cli.ts), loaded by ./cli.ts only when one of them is asked for.
2
- import { sessionTrustEnv } from './ca-trust.ts';
3
+
3
4
  import { optionValue, ownerOption } from './cli-args.ts';
4
- import { printReady } from './up-cli.ts';
5
+
5
6
  import { spawn, spawnSync } from 'node:child_process';
6
7
  import { createRequire } from 'node:module';
7
8
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
8
9
  import { tmpdir } from 'node:os';
9
10
  import { dirname, join, resolve, resolve as resolvePath } from 'node:path';
10
- import { activateScript, branchWorld, checkoutWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, resetWorld, seedWorld, worldOrigin, approveWorldChangeset, writeWorldClock, checkPrerequisites, coverWorld, createWorldChangeset, diffWorld, doctorWorld, downWorld, findWorldChangeset, formatCoverageReport, recordCoverage, formatInitReport, formatPrerequisiteChecks, formatProjectInspection, initWorld, inspectProject, isRemoteWorldRef, listWorldChangesets, listWorlds, markWorld, migrateWorldConfig, readReflectRoutes, reflectRoutesPath, replayWorldChangeset, resolveWorldRef, attachWorld, retireWorldConsumers, runWorld, shareWorldServices, shellWorld, startReflectFront, startReflectResolver, statusWorld, statusWorldChangeset, tailWorldActions, unshareWorld, upWorld, urlsWorld, urlWorld, verifyWorldChangeset, worldManifest, writeReflectRoutes, readReflectManifest, writeReflectManifest, clearReflectManifest, composeOverrideForReflect, splitDockerComposeArgs, dockerComposeWithOverride, ATTACHED_CA_PATH } from './index.ts';
11
- import { formatApplication, formatChangeset, formatRebaseReport, formatChangesetStatus, formatLedgerDelta, formatReplayReport, formatVerification, parseVerifierExpression } from '@volter/world-core/runtime';
12
- import { advertiseWorldManifest, fetchRemoteManifest, MANIFEST_PATH, remoteAttachEnv, startManifestServer } from './serve.ts';
11
+
12
+
13
+
13
14
  import type { WorldManifest } from './attach.ts';
14
- import { clockFile, instanceDir, pruneWorlds, withoutVaultSignIn } from './runtime.ts';
15
- import { worldResourceReport } from './resources.ts';
16
- import { worldConsumers, worldUsage } from './consumers.ts';
17
- import { addAppHosts, appUrlUnsetMessage, detectAppUrl, readAppUrl, setAppUrl } from './app-url.ts';
15
+
16
+
17
+
18
+
18
19
  import type { PrerequisiteId } from './index.ts';
19
- import { describeServiceEnd, describeWorldEvent, readServiceEnd, readWorldEvents } from './service-exit.ts';
20
- import { worldEventsPath } from './runtime.ts';
21
- import { resolveCatalog } from './catalog.ts';
22
- import { loadWorldConfig } from './configs.ts';
23
- import { vendorOfPackModule } from '@volter/world-core/runtime';
20
+
21
+
22
+
23
+
24
+
24
25
 
25
26
 
26
27
 
@@ -54,7 +55,7 @@ function acknowledgeOptions(args: string[], command: string): Record<string, str
54
55
 
55
56
  function printHelp(): void {
56
57
  process.stdout.write(`Usage:
57
- volter-world init <name> --repo <path> [--out <dir>] [--force] [--allow-unknown] [--acknowledge vendor=reason]... [--json] [--root <repo>]
58
+ volter-world init <name> --repo <path> [--out <dir>] [--needs <needs.json>] [--force] [--allow-unknown] [--acknowledge vendor=reason]... [--json] [--root <repo>]
58
59
  # the FRONT DOOR: detect the repo's vendors, EMIT a world config +
59
60
  # env file OUTSIDE the repo (default <repo>/../<name>-pilot/, the
60
61
  # repo is never written to), then run the coverage proof over the
@@ -62,6 +63,7 @@ function printHelp(): void {
62
63
  # Deterministic: same repo + same pack catalog => identical bytes.
63
64
  # init DIAGNOSES; it never boots — it prints the exact next commands
64
65
  volter-world up <config> --env-out=<path> [--owner <label>] [--name <name>] [--mode local|share|sealed] [--isolation process|colocated|worker] [--keep-state] [--app-url <url>] [--in-process] [--json] [--root <repo>]
66
+ volter-world up <config> --env-out=<path> --in-process --attach-after-line [--name <name>] [--keep-state] [--app-url <url>] [--root <repo>] -- <command...> # after its last boot line, waits for stdin "attach" before running the command
65
67
  volter-world outdated <world> [--json] — each mounted twin: pinned / mounted / catalog-current version and protocol standing (R17)
66
68
  volter-world audit <world> [--json] — each mounted twin's grade, as the catalog records it (R17)
67
69
  volter-world down <name> [--grace-ms <ms>] [--purge] [--if-instance <token>] [--root <repo>] # SIGTERM, then SIGKILL survivors after the grace (default 5000ms)
@@ -184,6 +186,7 @@ export async function main(): Promise<void> {
184
186
  }
185
187
 
186
188
  if (cmd === 'consumers') {
189
+ const { retireWorldConsumers } = await import('./runtime.ts');
187
190
  // `volter-world consumers retire <world> [--consumer <id>] [--root <repo>] [--json]`: the owner's
188
191
  // explicit resolution of attachments whose runner and command are provably gone.
189
192
  const args = rest.filter((arg): arg is string => arg !== undefined);
@@ -205,6 +208,9 @@ export async function main(): Promise<void> {
205
208
  }
206
209
 
207
210
  if (cmd === 'resources' || cmd === 'prune') {
211
+ const { pruneWorlds, statusWorld } = await import('./runtime.ts');
212
+ const { worldResourceReport } = await import('./resources.ts');
213
+ const { worldConsumers, worldUsage } = await import('./consumers.ts');
208
214
  const args = [subject, ...rest].filter((arg): arg is string => arg !== undefined);
209
215
  const reportRoot = optionValue(args, '--root', process.cwd());
210
216
  // Destructive commands must reject typos, not silently broaden a target to all Worlds.
@@ -263,6 +269,7 @@ export async function main(): Promise<void> {
263
269
 
264
270
 
265
271
  if (cmd === 'down') {
272
+ const { downWorld } = await import('./runtime.ts');
266
273
  if (!subject) throw new Error('volter-world down: missing world name');
267
274
  const graceArg = optionValue(rest, '--grace-ms');
268
275
  if (graceArg && !(Number(graceArg) >= 0)) throw new Error(`volter-world down: invalid --grace-ms "${graceArg}" (want a non-negative number)`);
@@ -298,6 +305,8 @@ export async function main(): Promise<void> {
298
305
  }
299
306
 
300
307
  if (cmd === 'events') {
308
+ const { statusWorld, worldEventsPath } = await import('./runtime.ts');
309
+ const { readWorldEvents, describeWorldEvent } = await import('./service-exit.ts');
301
310
  if (!subject) throw new Error('volter-world events: missing world name');
302
311
  const status = statusWorld(subject, root, { readOnly: true, light: true });
303
312
  const events = readWorldEvents(worldEventsPath(status.dirs.logs));
@@ -308,6 +317,9 @@ export async function main(): Promise<void> {
308
317
  }
309
318
 
310
319
  if (cmd === 'status') {
320
+ const { statusWorld, worldEventsPath } = await import('./runtime.ts');
321
+ const { worldUsage } = await import('./consumers.ts');
322
+ const { readServiceEnd, describeServiceEnd, readWorldEvents, describeWorldEvent } = await import('./service-exit.ts');
311
323
  if (!subject) throw new Error('volter-world status: missing world name');
312
324
  const status = statusWorld(subject, root);
313
325
  const usage = worldUsage(root, subject);
@@ -364,6 +376,7 @@ export async function main(): Promise<void> {
364
376
  // `plan` and `review` retired 2026-09-05: the changeset verbs are the one review path (verify, approve, status, push).
365
377
 
366
378
  if (cmd === 'doctor') {
379
+ const { doctorWorld } = await import('./runtime.ts');
367
380
  if (!subject) throw new Error('volter-world doctor: missing world name');
368
381
  const report = await doctorWorld(subject, { root, verifyPublic: rest.includes('--verify-public') });
369
382
  if (rest.includes('--json')) {
@@ -377,6 +390,7 @@ export async function main(): Promise<void> {
377
390
  }
378
391
 
379
392
  if (cmd === 'doctor-prereqs') {
393
+ const { checkPrerequisites, formatPrerequisiteChecks } = await import('./prerequisites.ts');
380
394
  const required = rest.flatMap((arg, index) => arg === '--require' ? [rest[index + 1]] : [])
381
395
  .filter(Boolean) as PrerequisiteId[];
382
396
  const checks = checkPrerequisites(required.length ? required : ['local-execution']);
@@ -392,6 +406,7 @@ export async function main(): Promise<void> {
392
406
  }
393
407
 
394
408
  if (cmd === 'inspect-project') {
409
+ const { inspectProject, formatProjectInspection } = await import('./project-inspect.ts');
395
410
  const args = [...(subject ? [subject] : []), ...rest];
396
411
  const path = args.find((arg) => !arg.startsWith('--')) || process.cwd();
397
412
  const report = inspectProject(path);
@@ -400,6 +415,7 @@ export async function main(): Promise<void> {
400
415
  }
401
416
 
402
417
  if (cmd === 'migrate-config') {
418
+ const { migrateWorldConfig } = await import('./configs.ts');
403
419
  if (!subject) throw new Error('volter-world migrate-config: missing config path or name');
404
420
  const migration = migrateWorldConfig(subject, root);
405
421
  if (rest.includes('--json')) process.stdout.write(`${JSON.stringify(migration, null, 2)}\n`);
@@ -430,20 +446,26 @@ export async function main(): Promise<void> {
430
446
  }
431
447
 
432
448
  if (cmd === 'init') {
449
+ const { initWorld, formatInitReport } = await import('./init.ts');
433
450
  if (!subject) throw new Error('volter-world init: missing world name (usage: volter-world init <name> --repo <path>)');
434
451
  const repo = optionValue(rest, '--repo');
435
452
  if (!repo) throw new Error('volter-world init: --repo <path> is required (the application repo to build the world for)');
436
453
  const out = optionValue(rest, '--out');
454
+ const needs = optionValue(rest, '--needs');
437
455
  const result = initWorld(subject, repo, {
438
456
  root,
439
457
  ...(out ? { out } : {}),
458
+ ...(needs ? { needs } : {}),
440
459
  force: rest.includes('--force'),
441
460
  allowUnknown: rest.includes('--allow-unknown'),
442
461
  acknowledge: acknowledgeOptions(rest, 'init'),
443
462
  });
444
463
  process.stdout.write(rest.includes('--json') ? `${JSON.stringify(result, null, 2)}\n` : formatInitReport(result));
445
464
  // Vendor coverage IS the exit code. App boot risks remain a separate, prominent report field.
446
- if (!result.ok) process.exit(1);
465
+ // A World made from a stack's needs is the exception, and says so: it is made for the services the stack
466
+ // names, a vendor with no twin is its report and not its failure, and its maker goes by this exit alone.
467
+ if (!result.ok && needs) process.stdout.write('\nVENDOR COVERAGE: incomplete (the vendors above with no pack). With --needs that is reported and is not a failure: the World is written, exit 0.\n');
468
+ if (!result.ok && !needs) process.exit(1);
447
469
  return;
448
470
  }
449
471
 
@@ -451,9 +473,10 @@ export async function main(): Promise<void> {
451
473
  // twins this world mounted; `audit` is each one's grade as the catalog records it. Both read
452
474
  // instance.json and the committed artifacts — nothing is fetched.
453
475
  if (cmd === 'outdated' || cmd === 'audit') {
476
+ const { statusWorld } = await import('./runtime.ts');
454
477
  if (!subject) throw new Error(`volter-world ${cmd}: missing world name (usage: volter-world ${cmd} <world> [--json])`);
455
478
  const status = statusWorld(subject, root);
456
- const rows = worldUpkeep(status, cmd, root);
479
+ const rows = await worldUpkeep(status, cmd, root);
457
480
  if (rest.includes('--json')) process.stdout.write(`${JSON.stringify(rows, null, 2)}\n`);
458
481
  else {
459
482
  if (rows.length === 0) process.stdout.write(`World ${status.name}: no twin mounted with a resolved version — boot it once (up) so instance.json records what was mounted\n`);
@@ -463,6 +486,7 @@ export async function main(): Promise<void> {
463
486
  return;
464
487
  }
465
488
  if (cmd === 'covers') {
489
+ const { coverWorld, formatCoverageReport, recordCoverage } = await import('./covers.ts');
466
490
  if (!subject) throw new Error('volter-world covers: missing world name (usage: volter-world covers <world> --repo <path>)');
467
491
  const repo = optionValue(rest, '--repo');
468
492
  if (!repo) throw new Error('volter-world covers: --repo <path> is required (the application repo to prove coverage for)');
@@ -481,6 +505,8 @@ export async function main(): Promise<void> {
481
505
  }
482
506
 
483
507
  if (cmd === 'clock') {
508
+ const { clockFile } = await import('./runtime.ts');
509
+ const { writeWorldClock } = await import('./clock.ts');
484
510
  // THE operator door for world time (physics): show / set <iso> / advance <duration> / shift <duration> / clear.
485
511
  // The clock every twin reads per request (kernel worldNow()), and every application process through the injector
486
512
  // (inject.cjs, WORLD TIME); a change takes effect live, no restarts. Its two forms (world-clock.cjs): `set` freezes
@@ -539,6 +565,7 @@ export async function main(): Promise<void> {
539
565
  }
540
566
 
541
567
  if (cmd === 'urls') {
568
+ const { urlsWorld } = await import('./runtime.ts');
542
569
  if (!subject) throw new Error('volter-world urls: missing world name');
543
570
  const urls = urlsWorld(subject, root);
544
571
  if (rest.includes('--json')) {
@@ -553,6 +580,7 @@ export async function main(): Promise<void> {
553
580
  }
554
581
 
555
582
  if (cmd === 'url') {
583
+ const { urlWorld } = await import('./runtime.ts');
556
584
  if (!subject) throw new Error('volter-world url: missing world name (usage: volter-world url <world> [service])');
557
585
  const [service] = positionalArgs(rest);
558
586
  const urls = urlWorld(subject, { root, ...(service === undefined ? {} : { service }) });
@@ -562,6 +590,7 @@ export async function main(): Promise<void> {
562
590
  }
563
591
 
564
592
  if (cmd === 'app-url') {
593
+ const { setAppUrl, detectAppUrl, addAppHosts, readAppUrl, appUrlUnsetMessage } = await import('./app-url.ts');
565
594
  if (!subject) throw new Error('volter-world app-url: missing world name (usage: volter-world app-url <world> [--set <url>|--detect <pid|port>])');
566
595
  const set = optionValue(rest, '--set');
567
596
  const detect = optionValue(rest, '--detect');
@@ -592,6 +621,7 @@ export async function main(): Promise<void> {
592
621
  }
593
622
 
594
623
  if (cmd === 'tail' || cmd === 'log') {
624
+ const { tailWorldActions } = await import('./tail.ts');
595
625
  if (!subject) throw new Error(`volter-world ${cmd}: missing world name (usage: volter-world log <world> [service...])`);
596
626
  const services = positionalArgs(rest);
597
627
  const controller = new AbortController();
@@ -610,6 +640,7 @@ export async function main(): Promise<void> {
610
640
  }
611
641
 
612
642
  if (cmd === 'seed' || cmd === 'reset') {
643
+ const { seedWorld, resetWorld } = await import('./placeholder.ts');
613
644
  if (!subject) throw new Error(`volter-world ${cmd}: missing world name (usage: volter-world ${cmd} <world> [--entry <file>] [--cwd <dir>])`);
614
645
  if (rest.some(arg => arg === '--env-file' || arg.startsWith('--env-file='))) throw new Error('--env-file is not an input for this command. Reset accepts --env-out=<new-output-path>.');
615
646
  const envFile = optionValue(rest, '--env-out') || undefined;
@@ -624,6 +655,8 @@ export async function main(): Promise<void> {
624
655
  }
625
656
 
626
657
  if (cmd === 'branch') {
658
+ const { branchWorld } = await import('./branch.ts');
659
+ const { printReady } = await import('./up-cli.ts');
627
660
  if (rest.some(arg => arg === '--env-file' || arg.startsWith('--env-file='))) throw new Error('--env-file is not an input for this command. Use --env-out=<new-output-path>; never pass credentials or custody as an output.');
628
661
  const [name] = positionalArgs(rest);
629
662
  if (!subject || !name) throw new Error('volter-world branch: usage: volter-world branch <world> <name> [--env-out=<path>]');
@@ -634,12 +667,15 @@ export async function main(): Promise<void> {
634
667
  return;
635
668
  }
636
669
  if (cmd === 'checkout') {
670
+ const { checkoutWorld } = await import('./branch.ts');
671
+ const { printReady } = await import('./up-cli.ts');
637
672
  if (!subject) throw new Error('volter-world checkout: missing world name');
638
673
  const instance = await checkoutWorld(subject, { root });
639
674
  if (rest.includes('--json')) process.stdout.write(`${JSON.stringify({ name: instance.name, instance: instance.instance, createdAt: instance.createdAt }, null, 2)}\n`); else printReady(instance);
640
675
  return;
641
676
  }
642
677
  if (cmd === 'fetch') {
678
+ const { fetchFromOrigin } = await import('./origin.ts');
643
679
  if (!subject) throw new Error('volter-world fetch: missing world name (usage: volter-world fetch <world> --key <key> [--origin <url> --namespace <org>/<world>] [--full])');
644
680
  const key = optionValue(rest, '--key') || process.env.VOLTER_TWINS_READ_KEY || process.env.VOLTER_TWINS_KEY || '';
645
681
  if (!key) throw new Error('volter-world fetch: --key <twins key> is required (the read key for a fetch, the admin key for --full)');
@@ -652,6 +688,7 @@ export async function main(): Promise<void> {
652
688
  return;
653
689
  }
654
690
  if (cmd === 'origin') {
691
+ const { worldOrigin } = await import('./origin.ts');
655
692
  if (!subject) throw new Error('volter-world origin: missing world name');
656
693
  const origin = worldOrigin(subject, root);
657
694
  if (rest.includes('--json')) process.stdout.write(`${JSON.stringify(origin, null, 2)}\n`);
@@ -660,6 +697,7 @@ export async function main(): Promise<void> {
660
697
  }
661
698
 
662
699
  if (cmd === 'mark') {
700
+ const { markWorld } = await import('./changeset.ts');
663
701
  if (!subject) throw new Error('volter-world mark: missing world name (usage: volter-world mark <world> [--id <marker>] [--note <text>])');
664
702
  const id = optionValue(rest, '--id');
665
703
  const note = optionValue(rest, '--note');
@@ -670,6 +708,8 @@ export async function main(): Promise<void> {
670
708
  }
671
709
 
672
710
  if (cmd === 'diff') {
711
+ const { diffWorld } = await import('./changeset.ts');
712
+ const { formatLedgerDelta } = await import('@volter/world-core/runtime');
673
713
  if (!subject) throw new Error('volter-world diff: missing world name (usage: volter-world diff <world> [--base <marker>])');
674
714
  const base = optionValue(rest, '--base');
675
715
  const delta = diffWorld(subject, { root, ...(base ? { base } : {}) });
@@ -678,6 +718,8 @@ export async function main(): Promise<void> {
678
718
  }
679
719
 
680
720
  if (cmd === 'changeset') {
721
+ const { parseVerifierExpression, formatChangeset, formatReplayReport, formatVerification, formatChangesetStatus, formatRebaseReport, formatApplication } = await import('@volter/world-core/runtime');
722
+ const { createWorldChangeset, findWorldChangeset, listWorldChangesets, replayWorldChangeset, verifyWorldChangeset, approveWorldChangeset, statusWorldChangeset, rebaseWorldChangeset, pushWorldChangeset } = await import('./changeset.ts');
681
723
  const CHANGESET_VALUE_FLAGS = ['--root', '--base', '--into', '--world', '--verifier', '--as', '--note'];
682
724
  const args = positionalArgs(rest, CHANGESET_VALUE_FLAGS);
683
725
  const world = optionValue(rest, '--world');
@@ -773,6 +815,8 @@ export async function main(): Promise<void> {
773
815
  }
774
816
 
775
817
  if (cmd === 'list') {
818
+ const { listWorlds } = await import('./runtime.ts');
819
+ const { worldUsage } = await import('./consumers.ts');
776
820
  const listArgs = [subject, ...rest].filter((arg): arg is string => arg !== undefined);
777
821
  const listRoot = optionValue(listArgs, '--root', process.cwd());
778
822
  const worlds = listWorlds(listRoot).map(world => ({ ...world, usage: worldUsage(listRoot, world.name) }));
@@ -785,6 +829,7 @@ export async function main(): Promise<void> {
785
829
  }
786
830
 
787
831
  if (cmd === 'env') {
832
+ const { attachWorld } = await import('./runtime.ts');
788
833
  if (!subject) throw new Error('volter-world env: missing world name');
789
834
  const split = rest.indexOf('--');
790
835
  if (split < 0) throw new Error('volter-world env: pass command after --');
@@ -793,6 +838,10 @@ export async function main(): Promise<void> {
793
838
  }
794
839
 
795
840
  if (cmd === 'attach') {
841
+ // The local env attach, every app's CMD path, needs the World's ref and its runtime alone: the two load together,
842
+ // and the remote door (serve.ts) and the reflect front (reflect.ts, ca-trust.ts) load only on their own paths.
843
+ // All five were imported one after another first (S2 R16: Dub's attach, 225 bodies in 341 ms before its `next`).
844
+ const [{ resolveWorldRef, isRemoteWorldRef, worldManifest }, { withoutVaultSignIn, instanceDir, attachWorld }] = await Promise.all([import('./attach.ts'), import('./runtime.ts')]);
796
845
  // `subject` may be the ref, a flag, or the `--` separator — reassemble and split.
797
846
  const args = [subject, ...rest].filter((arg): arg is string => arg !== undefined);
798
847
  const split = args.indexOf('--');
@@ -807,6 +856,7 @@ export async function main(): Promise<void> {
807
856
  throw new Error('--owner is supported for local attach --via env commands');
808
857
  }
809
858
  if (isRemoteWorldRef(resolved.ref)) {
859
+ const { fetchRemoteManifest, remoteAttachEnv } = await import('./serve.ts');
810
860
  // remote world: fetch the manifest, synthesize the attach env, exec
811
861
  const attachToken = optionValue(head, '--token') || process.env.VOLTER_WORLD_TOKEN || undefined;
812
862
  const manifest = await fetchRemoteManifest(resolved.ref, attachToken === undefined ? {} : { token: attachToken });
@@ -849,6 +899,7 @@ export async function main(): Promise<void> {
849
899
  // gets the override — and everything else runs with the trust env and VOLTER_WORLD set, its DNS
850
900
  // being the caller's to point (a container's --dns, a resolver on the host).
851
901
  if (command.length === 0) throw new Error('volter-world attach: pass the command after --');
902
+ const [{ readReflectManifest, splitDockerComposeArgs, composeOverrideForReflect, dockerComposeWithOverride, ATTACHED_CA_PATH }, { sessionTrustEnv }] = await Promise.all([import('./reflect.ts'), import('./ca-trust.ts')]);
852
903
  const manifest = readReflectManifest(attachRoot, resolved.ref);
853
904
  if (!manifest) throw new Error(`volter-world attach --via reflect: no reflect front is running for world ${resolved.ref} — start one: volter-world reflect ${resolved.ref} --target-ip <ip> --port 443 --resolver-port 53`);
854
905
  const env: Record<string, string> = { ...withoutVaultSignIn(process.env) as Record<string, string>, VOLTER_WORLD: resolved.ref, VOLTER_REFLECT_DNS: manifest.targetIp, VOLTER_WORLD_CA: manifest.caCertPath };
@@ -879,12 +930,15 @@ export async function main(): Promise<void> {
879
930
  }
880
931
 
881
932
  if (cmd === 'manifest') {
933
+ const { worldManifest } = await import('./attach.ts');
882
934
  if (!subject) throw new Error('volter-world manifest: missing world name');
883
935
  process.stdout.write(`${JSON.stringify(worldManifest(subject, root), null, 2)}\n`);
884
936
  return;
885
937
  }
886
938
 
887
939
  if (cmd === 'reflect') {
940
+ const { reflectRoutesPath, startReflectFront, startReflectResolver, readReflectRoutes, writeReflectManifest, clearReflectManifest } = await import('./reflect.ts');
941
+ const { statusWorld, instanceDir } = await import('./runtime.ts');
888
942
  if (!subject) throw new Error('volter-world reflect: missing world name');
889
943
  const targetIp = optionValue(rest, '--target-ip');
890
944
  if (!targetIp) throw new Error('volter-world reflect: --target-ip <ip> is required (the address attachers reach the front at)');
@@ -918,6 +972,9 @@ export async function main(): Promise<void> {
918
972
  }
919
973
 
920
974
  if (cmd === 'serve') {
975
+ const { startReflectFront } = await import('./reflect.ts');
976
+ const { statusWorld, instanceDir } = await import('./runtime.ts');
977
+ const { startManifestServer, advertiseWorldManifest, MANIFEST_PATH } = await import('./serve.ts');
921
978
  if (!subject) throw new Error('volter-world serve: missing world name');
922
979
  const advertise = optionValue(rest, '--advertise');
923
980
  if (!advertise) throw new Error('volter-world serve: --advertise <https-origin> is required (how attachers reach the door, e.g. https://worlds.corp:8443)');
@@ -956,6 +1013,7 @@ export async function main(): Promise<void> {
956
1013
  }
957
1014
 
958
1015
  if (cmd === 'route') {
1016
+ const { reflectRoutesPath, readReflectRoutes, writeReflectRoutes } = await import('./reflect.ts');
959
1017
  if (!subject) throw new Error('volter-world route: missing world name');
960
1018
  const [verb, host] = rest.filter((arg) => !arg.startsWith('--'));
961
1019
  const routesPath = reflectRoutesPath(root, subject);
@@ -974,17 +1032,21 @@ export async function main(): Promise<void> {
974
1032
  }
975
1033
 
976
1034
  if (cmd === 'activate') {
1035
+ const { activateScript } = await import('./runtime.ts');
977
1036
  if (!subject) throw new Error('volter-world activate: missing world name');
978
1037
  process.stdout.write(activateScript(subject, root));
979
1038
  return;
980
1039
  }
981
1040
 
982
1041
  if (cmd === 'shell') {
1042
+ const { shellWorld } = await import('./runtime.ts');
983
1043
  if (!subject) throw new Error('volter-world shell: missing world name');
984
1044
  process.exit(await shellWorld(subject, root));
985
1045
  }
986
1046
 
987
1047
  if (cmd === 'run') {
1048
+ const { runWorld } = await import('./runtime.ts');
1049
+ const { readServiceEnd, describeServiceEnd } = await import('./service-exit.ts');
988
1050
  if (!subject) throw new Error('volter-world run: missing config');
989
1051
  const split = rest.indexOf('--');
990
1052
  if (split < 0) throw new Error('volter-world run: pass command after --');
@@ -1013,6 +1075,7 @@ export async function main(): Promise<void> {
1013
1075
  }
1014
1076
 
1015
1077
  if (cmd === 'share') {
1078
+ const { shareWorldServices } = await import('./runtime.ts');
1016
1079
  if (!subject) throw new Error('volter-world share: missing world name');
1017
1080
  const service = optionValue(rest, '--service') || undefined;
1018
1081
  const verifyPath = rest.includes('--no-verify')
@@ -1044,6 +1107,7 @@ export async function main(): Promise<void> {
1044
1107
  }
1045
1108
 
1046
1109
  if (cmd === 'unshare') {
1110
+ const { unshareWorld } = await import('./runtime.ts');
1047
1111
  if (!subject) throw new Error('volter-world unshare: missing world name');
1048
1112
  const service = optionValue(rest, '--service') || undefined;
1049
1113
  const result = await unshareWorld(subject, { root, service });
@@ -1059,7 +1123,10 @@ export async function main(): Promise<void> {
1059
1123
  /** The rows `outdated` / `audit` print. `outdated`: pinned vs mounted vs catalog-current version and the
1060
1124
  * protocol standing; `audit`: each mounted twin's grade, from the catalog's record. `attention` marks a row the
1061
1125
  * operator should act on (exit 1). */
1062
- function worldUpkeep(status: ReturnType<typeof statusWorld>, kind: 'outdated' | 'audit', root: string): Array<{ id: string; line: string; attention: boolean }> {
1126
+ async function worldUpkeep(status: ReturnType<typeof statusWorld>, kind: 'outdated' | 'audit', root: string): Promise<Array<{ id: string; line: string; attention: boolean }>> {
1127
+ const { resolveCatalog } = await import('./catalog.ts');
1128
+ const { loadWorldConfig } = await import('./configs.ts');
1129
+ const { vendorOfPackModule } = await import('@volter/world-core/runtime');
1063
1130
  const catalog = resolveCatalog(root);
1064
1131
  const config = loadWorldConfig(status.name, root).config;
1065
1132
  const out: Array<{ id: string; line: string; attention: boolean }> = [];
@@ -0,0 +1,2 @@
1
+ // Imported first by the CLI: measures only its modules, after the engine starts this first body.
2
+ export const cliLoadStarted = performance.now();
package/src/cli.ts CHANGED
@@ -1,12 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  // volter-world: each command loads only what it needs. `up` (./up-cli.ts) starts the World's owner process
3
3
  // before any other command's modules load; every other command is ./cli-commands.ts.
4
+ import { cliLoadStarted } from './cli-load-clock.ts';
4
5
  import { optionValue } from './cli-args.ts';
5
6
  import { errorMessage } from './error-message.ts';
6
7
 
7
8
  const [cmd, subject, ...rest] = process.argv.slice(2);
8
9
  const run = cmd === 'up' && subject !== '--help' && subject !== '-h'
9
- ? import('./up-cli.ts').then(({ upCommand }) => upCommand(subject, rest, optionValue(rest, '--root', process.cwd())))
10
+ ? import('./up-cli.ts').then(({ upCommand }) => {
11
+ process.stderr.write(`volter-world up: entry modules loaded in ${(performance.now() - cliLoadStarted).toFixed(1)} ms\n`);
12
+ return upCommand(subject, rest, optionValue(rest.includes('--attach-after-line') && rest.includes('--') ? rest.slice(0, rest.indexOf('--')) : rest, '--root', process.cwd()));
13
+ })
10
14
  : import('./cli-commands.ts').then(({ main }) => main());
11
15
  run.catch((error) => {
12
16
  process.stderr.write(`${errorMessage(error)}\n`);
@@ -0,0 +1,70 @@
1
+ // ClickHouse's managed ownership questions: HTTP ping, this child's kernel boot answer,
2
+ // and a native Hello/Ping. No SQL is executed by readiness.
3
+ // Packet fields: clickhouse-docs/docs/native-protocol/{client,server,basics}.md;
4
+ // revision54429 uses the reached clickhouse-driver0.2.10 layout retained with the pack.
5
+ import { connect } from 'node:net';
6
+ import { WORLD_BOOT_PATH } from '@volter/world-core/runtime';
7
+ import type { ManagedProtocol } from './twin-backing.ts';
8
+ import type { InfraService } from './infra-definition.ts';
9
+
10
+ async function answer(port: number, path: string): Promise<string | undefined> {
11
+ const controller = new AbortController(), timer = setTimeout(() => controller.abort(), 1000);
12
+ try {
13
+ const response = await fetch(`http://127.0.0.1:${port}${path}`, { signal: controller.signal, redirect: 'error' });
14
+ if (!response.ok || !response.body) return undefined;
15
+ // This private proof contains a UUID boot nonce and a JavaScript-safe PID; ping is four bytes.
16
+ const reader = response.body.getReader(); let text = '', bytes = 0;
17
+ try { while (true) { const part = await reader.read(); if (part.done) break; bytes += part.value.length; if (bytes > 256) return undefined; text += new TextDecoder().decode(part.value); } }
18
+ finally { await reader.cancel().catch(() => undefined); }
19
+ return text;
20
+ } catch { return undefined; } finally { clearTimeout(timer); }
21
+ }
22
+ function number(value: number): number[] { const result: number[] = []; do { const byte = value % 128; value = Math.floor(value / 128); result.push(byte | (value ? 128 : 0)); } while (value); return result; }
23
+ function string(value: string): number[] { const bytes = new TextEncoder().encode(value); return [...number(bytes.length), ...bytes]; }
24
+ function nativeAnswers(service: InfraService): Promise<boolean> {
25
+ const port = service.sides?.native?.hostPort; if (!port) return Promise.resolve(false);
26
+ const users = service.configuration?.users as Record<string, string> | undefined;
27
+ const [user, password] = users && Object.hasOwn(users, 'default') ? ['default', users.default!] : Object.entries(users ?? { default: '' })[0] ?? [];
28
+ if (typeof user !== 'string' || typeof password !== 'string') return Promise.resolve(false);
29
+ const hello = Uint8Array.from([0, ...string('World ownership'), 1, 0, ...number(54429), ...string('default'), ...string(user), ...string(password)]);
30
+ return new Promise(resolve => {
31
+ const socket = connect({ host: '127.0.0.1', port, timeout: 1000 }); let bytes = new Uint8Array(), phase = 'hello', settled = false;
32
+ const end = (valid: boolean) => { if (settled) return; settled = true; clearTimeout(timer); socket.destroy(); resolve(valid); };
33
+ // One complete ownership question has one deadline; incoming fragments never extend it.
34
+ const timer = setTimeout(() => end(false), 1000);
35
+ const missing = Symbol('incomplete');
36
+ socket.once('connect', () => socket.write(hello));
37
+ socket.on('data', chunk => {
38
+ // The declaring pack fixes its Hello name, timezone and display name;64KiB is beyond that packet domain.
39
+ if (bytes.length + chunk.length > 65536) { end(false); return; }
40
+ const joined = new Uint8Array(bytes.length + chunk.length); joined.set(bytes); joined.set(chunk, bytes.length); bytes = joined;
41
+ let at = 0;
42
+ const read = (): number => {
43
+ let value = 0n;
44
+ for (let shift = 0; shift < 70; shift += 7) { if (at >= bytes.length) throw missing; const byte = bytes[at++]!; value |= BigInt(byte & 127) << BigInt(shift); if (!(byte & 128)) { if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error('Invalid native integer'); return Number(value); } }
45
+ throw new Error('Invalid native integer');
46
+ };
47
+ const text = (): string => { const length = read(); if (length > bytes.length - at) throw missing; const result = new TextDecoder().decode(bytes.subarray(at, at + length)); at += length; return result; };
48
+ try {
49
+ if (phase === 'hello') {
50
+ if (read() !== 0 || text() !== 'ClickHouse') { end(false); return; }
51
+ read(); read(); const revision = read(); if (revision !== 54429) { end(false); return; }
52
+ text(); text(); read(); bytes = bytes.slice(at); phase = 'pong'; socket.write(Uint8Array.of(4));
53
+ if (!bytes.length) return; at = 0;
54
+ }
55
+ end(read() === 4);
56
+ } catch (error) { if (error !== missing) end(false); }
57
+ });
58
+ socket.once('error', () => end(false)); socket.once('timeout', () => end(false)); socket.once('close', () => end(false));
59
+ });
60
+ }
61
+ export const CLICKHOUSE_PROTOCOL: ManagedProtocol = {
62
+ name: 'ClickHouse', bootProof: true,
63
+ async answers(port) { return await answer(port, '/ping') === 'Ok.\n'; },
64
+ async serves(port, pid, service, boot) {
65
+ if (!service || !boot || await answer(port, '/ping') !== 'Ok.\n') return false;
66
+ const body = await answer(port, WORLD_BOOT_PATH); if (!body) return false;
67
+ try { const held = JSON.parse(body); if (held.boot !== boot || held.pid !== pid) return false; } catch { return false; }
68
+ return nativeAnswers(service);
69
+ },
70
+ };
@@ -4,7 +4,7 @@ export const COMMAND_GRACE_MS: number;
4
4
  export const COMMAND_CONFIRM_MS: number;
5
5
  export const commandProcessGroup: boolean;
6
6
  export class CommandRetirementError extends Error {}
7
- export function commandLifetime(child: ChildProcess, abort?: AbortSignal, preserveOnSuccess?: boolean): {
7
+ export function commandLifetime(child: ChildProcess, abort?: AbortSignal, preserveOnSuccess?: boolean, graceMs?: number): {
8
8
  identity: Promise<SpawnIdentity | undefined>;
9
9
  cancel(signal: NodeJS.Signals): void;
10
10
  readonly forwarded: NodeJS.Signals | undefined;
@@ -9,7 +9,10 @@ export class CommandRetirementError extends Error {}
9
9
 
10
10
  /** The child owns a private POSIX group. A bounded retirement requires both closed output and
11
11
  * a vanished group. Unknown retirement keeps the caller's ownership record and reservation. */
12
- export function commandLifetime(child, abort, preserveOnSuccess = false) {
12
+ /** `graceMs` is how long a cancelled child is awaited before SIGKILL. A command gets the default; a STORE (a
13
+ * database, a twin that holds state) is given its lifecycle's budget, since what it does between the signal and
14
+ * its exit is write its state down, and a SIGKILL in the middle of that leaves a store nothing may take. */
15
+ export function commandLifetime(child, abort, preserveOnSuccess = false, graceMs = COMMAND_GRACE_MS) {
13
16
  const identity = child.pid && commandProcessGroup ? childIdentity(child) : Promise.resolve(undefined);
14
17
  let signalFailure;
15
18
  let pendingSignals = 0;
@@ -62,7 +65,7 @@ export function commandLifetime(child, abort, preserveOnSuccess = false) {
62
65
  }
63
66
  };
64
67
  const watch = () => {
65
- if (!deadline) deadline = Date.now() + COMMAND_GRACE_MS;
68
+ if (!deadline) deadline = Date.now() + graceMs;
66
69
  timer ??= setInterval(check, 20);
67
70
  check();
68
71
  };
package/src/configs.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { copyFileSync, constants, existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
2
  import { basename, dirname, join, resolve } from 'node:path';
3
- import { getActiveWorldStore, stateDirName } from '@volter/world-core/runtime';
3
+ import { getActiveWorldStore, stateDirName } from '@volter/world-core/runtime-state';
4
4
  import { assertWorldConfig, LEGACY_WORLD_SELECTION, worldConfigDocument, type WorldChangesetRules, type WorldConfig } from './schema.ts';
5
5
 
6
6
  export function resolveConfigPath(config: string, root = process.cwd()): string {
package/src/consumers.ts CHANGED
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { hostname } from 'node:os';
4
4
  import { join } from 'node:path';
5
- import { stateDirName, withFileLock } from '@volter/world-core/runtime';
5
+ import { stateDirName, withFileLock } from '@volter/world-core/runtime-state';
6
6
 
7
7
  export type WorldConsumer = {
8
8
  id: string;
package/src/covers.ts CHANGED
@@ -627,6 +627,8 @@ export function worldTwinInventory(world: string, root: string): WorldTwinInvent
627
627
  export type CoverageOptions = {
628
628
  root?: string;
629
629
  allowUnknown?: boolean;
630
+ /** Application environment names already read from its declared stack. */
631
+ declaredEnvironmentNames?: readonly string[];
630
632
  /**
631
633
  * vendor → reason: vendors the operator ACKNOWLEDGES need no twin in this world — e.g. an SDK
632
634
  * used verifier-only (svix's Webhook verifies incoming signatures locally and never calls out).
@@ -685,11 +687,11 @@ function usesOf(signals: Map<string, string[]>): Map<string, Array<{ usage: Worl
685
687
 
686
688
  /** Detect every vendor (and every unmapped external-service-shaped signal) an application repo
687
689
  * betrays: mapped npm dependencies across the root + all workspace members, vendor-shaped env
688
- * names in committed `.env*` files, the SMTP/raw-protocol signal, and literal vendor URLs passed
690
+ * names in committed `.env*` files or its declared stack, the SMTP/raw-protocol signal, and literal vendor URLs passed
689
691
  * directly to fetch in production source. URL attribution reads the injector's VENDOR_HOSTS table
690
692
  * rather than re-encoding host rules here. Insertion order follows the sorted dependency, env-name,
691
693
  * and source lists, so callers that preserve it are deterministic. */
692
- export function detectRepoVendors(repoPath: string, services: Array<WorldServiceConfig | WorldServiceInstance> = []): RepoVendorSignals {
694
+ export function detectRepoVendors(repoPath: string, services: Array<WorldServiceConfig | WorldServiceInstance> = [], declaredEnvironmentNames: readonly string[] = []): RepoVendorSignals {
693
695
  // Application aliases are names the selected backing actually injects, never global vendor adoption.
694
696
  const backingEnvVendors = new Map<string, string>();
695
697
  for (const service of services) {
@@ -703,7 +705,10 @@ export function detectRepoVendors(repoPath: string, services: Array<WorldService
703
705
  // A Volter package a pack names as its vendor's SDK is one (`@volter/identity`, the Volter identity
704
706
  // service's client, is the volteridentity twin's), as any vendor's SDK is.
705
707
  const dependencies = projectDependencies(repo).filter((dep) => Object.hasOwn(SDK_TWINS, dep) || (!dep.startsWith('@volter/') && !dep.startsWith('pypi:volter')));
706
- const envNames = projectEnvNames(repo);
708
+ for (const name of declaredEnvironmentNames) {
709
+ if (typeof name !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error('Declared vendor environment name must be an identifier');
710
+ }
711
+ const envNames = [...new Set([...projectEnvNames(repo), ...declaredEnvironmentNames])].sort();
707
712
  const npmRegistries = projectNpmRegistries(repo);
708
713
 
709
714
  // vendor → detection sources
@@ -871,7 +876,7 @@ export function coverWorld(world: string, repoPath: string, options: CoverageOpt
871
876
  const repo = resolve(repoPath);
872
877
  const inventory = worldTwinInventory(world, root);
873
878
 
874
- const { detected, unknown: unknownSources, uses, unknownUses } = detectRepoVendors(repo, inventory.services);
879
+ const { detected, unknown: unknownSources, uses, unknownUses } = detectRepoVendors(repo, inventory.services, options.declaredEnvironmentNames);
875
880
 
876
881
  // Does ANY injector vendor key exist for this vendor? Decides whether an UNINTERCEPTABLE
877
882
  // finding is acknowledgeable: a key that exists means the twin COULD be intercepted and the