@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
package/dist/src/init.js CHANGED
@@ -1,3 +1,7 @@
1
+ import { copyNativeServerInputs, readNativeServerInputs, nativeServerRuntimeReference } from "./native-server-inputs.js";
2
+ import { goMysqlBinding, renderGoMysqlBinding } from "./go-mysql-binding.js";
3
+ import * as managedOriginalInputs from '@volter/world-core/managed-original-inputs';
4
+ import { copyOriginalInputSnapshot, readOriginalInputSnapshot } from "./original-inputs.js";
1
5
  // `volter-world init <name> --repo <path>` — the DETERMINISTIC FRONT DOOR of world adoption.
2
6
  //
3
7
  // Everything it needs already existed, in three pieces that no one command composed:
@@ -33,6 +37,7 @@
33
37
  // with a remediation line each. App-boot risks are a separate, prominent signal: an empty
34
38
  // example value read by production source can block before listen even when vendor coverage is
35
39
  // green. `init` never boots the world: it diagnoses, the operator (or the room) boots.
40
+ import { createHash } from 'node:crypto';
36
41
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
37
42
  import { PROTOCOL_MAJOR, stateDirName } from '@volter/world-core/runtime';
38
43
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
@@ -40,7 +45,7 @@ import { coverWorld, detectRepoVendors, envNameVendor, coverageRecord, formatCov
40
45
  import { fakeEnvValue, isAppKeyName, isGoogleOAuthClientEnvName, isGoogleServiceAccountEnvName } from "./fixture-env.js";
41
46
  import { findInstalledPackage, packCli, packEntry, resolveCatalog } from "./catalog.js";
42
47
  import { CATALOG_INDEX_PACKAGE, defaults as catalogDefaults } from "./catalog-index.js";
43
- import { projectEnvReads, projectManifestDirs, projectMigrationTools } from "./project-inspect.js";
48
+ import { filesLeftAsThePersonsOwn, projectEnvReads, projectManifestDirs, projectMigrationTools, readOnlyWhatTheSourceHolds, sourceText } from "./project-inspect.js";
44
49
  import { overlayEndpointEnv, packFacts } from "./pack-facts.js";
45
50
  import { VENDOR_SERVER_BACKINGS } from "./backings.js";
46
51
  import { APP_URL_ENV, DEFAULT_WORLD_SELECTION, ISSUE_ENV_PREFIX, MINT_ENV, worldConfigDocument, worldSelectionIncludes } from "./schema.js";
@@ -84,6 +89,96 @@ function standInStem(name, value, detected) {
84
89
  }
85
90
  /** Names that carry the application's own origin: its base URL (Next's public one included) and its auth library's. */
86
91
  const APP_ORIGIN_ENV = /^(?:NEXT_PUBLIC_)?(?:APP_)?(?:BASE_)?URL$|^NEXT_PUBLIC_APP_(?:BASE_)?URL$|^(?:NEXTAUTH|BETTER_AUTH|AUTH)_URL$/;
92
+ /** The families the image builder names (its SERVICE_FAMILIES) that are a managed kind here. */
93
+ const FAMILY_KIND = { postgres: 'postgres', mysql: 'mysql', mongodb: 'mongodb', redis: 'redis', search: 'search', clickhouse: 'clickhouse', memcached: 'memcached' };
94
+ const CONVENTIONAL_NAMES = { postgres: ['DATABASE_URL'], mysql: ['DATABASE_URL'], mongodb: ['MONGODB_URI'], redis: ['REDIS_URL'], clickhouse: ['CLICKHOUSE_URL'] };
95
+ /** The needs file, checked for its kind: a file of another shape is refused by name, never half-read. */
96
+ function readStackNeeds(path) {
97
+ let document;
98
+ try {
99
+ document = JSON.parse(readFileSync(path, 'utf8'));
100
+ }
101
+ catch (error) {
102
+ throw new Error(`volter init: --needs ${path} could not be read as JSON: ${error instanceof Error ? error.message : String(error)}`);
103
+ }
104
+ if (document.kind !== 'browser-substrate.stack-needs' || (document.version !== 1 && document.version !== 2 && document.version !== 3 && document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7) || !Array.isArray(document.needs)) {
105
+ throw new Error(`volter init: --needs ${path} is not a stack-needs file (kind "browser-substrate.stack-needs", version 1, 2, 3, 4, 5, 6 or 7)`);
106
+ }
107
+ const identifier = (name, what) => {
108
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
109
+ throw new Error(`volter init: --needs ${path} names a variable that is not an identifier among ${what}: ${JSON.stringify(name)}`);
110
+ return name;
111
+ };
112
+ const record = (value) => value && typeof value === 'object' && !Array.isArray(value) ? Object.entries(value) : [];
113
+ return {
114
+ composeFile: typeof document.composeFile === 'string' ? document.composeFile : 'the project\'s Compose file',
115
+ ...(typeof document.application?.service === 'string' ? { application: document.application.service } : {}),
116
+ // The application's own variables that name no service: its settings as the stack writes them, and those whose
117
+ // value the stack makes from a variable nothing sets, which are named and have no value here.
118
+ settings: record(document.settings).filter(([, value]) => typeof value === 'string').map(([name, value]) => ({ name: identifier(name, 'the application\'s settings'), value: value })),
119
+ unset: record(document.unset).map(([name]) => identifier(name, 'the application\'s unset variables')),
120
+ needs: document.needs.filter((need) => typeof need.service === 'string').map((need) => ({
121
+ service: need.service,
122
+ ...(need.originalInputs === undefined ? {} : { originalInputs: (() => {
123
+ if (document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7)
124
+ throw new Error('volter init: original inputs require needs version4,5,6 or7');
125
+ const reference = managedOriginalInputs.referenceOf(need.originalInputs);
126
+ const source = readOriginalInputSnapshot(dirname(resolve(path)), reference).snapshot.manifest;
127
+ const image = source.start.service.image;
128
+ if (source.service !== need.service || source.family !== need.family || typeof need.image !== 'string' || image?.raw !== need.image)
129
+ throw new Error('volter init: original inputs belong to another declared service');
130
+ return reference;
131
+ })() }),
132
+ ...(need.nativeServerInputs === undefined ? {} : (() => {
133
+ if (document.version !== 5 && document.version !== 7)
134
+ throw new Error('volter init: native managed input requires needs5 or7');
135
+ const checked = readNativeServerInputs(dirname(resolve(path)), need.nativeServerInputs);
136
+ const source = JSON.parse(checked.files.get('declaration.json').toString('utf8'));
137
+ if (need.family !== 'mysql' || need.supplier !== 'a World' || source.name !== need.service || source.image?.raw !== need.image)
138
+ throw new Error('volter init: native managed source belongs to another declared service');
139
+ return { nativeServerInputs: checked.reference, nativeCredentialAliases: checked.aliases };
140
+ })()),
141
+ ...(typeof need.image === 'string' ? { image: need.image } : {}),
142
+ ...(typeof need.family === 'string' ? { family: need.family } : {}),
143
+ names: (Array.isArray(need.names) ? need.names : []).map((entry) => {
144
+ // A name becomes a key of the World's environment file: an identifier, or it is refused by name. Anything
145
+ // else (a newline, an `=`) would write lines of its own there.
146
+ if (typeof entry.name !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(entry.name))
147
+ throw new Error(`volter init: --needs ${path} names a variable that is not an identifier under service ${String(need.service)}: ${JSON.stringify(entry.name)}`);
148
+ if (entry.containerPort !== undefined && ((document.version !== 3 && document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7) || !Number.isInteger(entry.containerPort) || Number(entry.containerPort) < 1 || Number(entry.containerPort) > 65535))
149
+ throw new Error(`volter init: --needs ${path} needs version3 and a valid declared vendor port`);
150
+ return { name: entry.name, ...goMysqlBinding(entry, document.version, entry.name, COMPOSE_INFRA.mysql.containerPort), ...(entry.containerPort !== undefined ? { containerPort: Number(entry.containerPort) } : {}), ...(typeof entry.scheme === 'string' && /^[a-z][a-z0-9+.-]*$/i.test(entry.scheme) ? { scheme: entry.scheme } : {}), ...(typeof entry.query === 'string' ? { query: entry.query } : {}), ...(entry.withPort === true ? { withPort: true } : {}) };
151
+ }),
152
+ components: (() => {
153
+ if (need.components === undefined)
154
+ return [];
155
+ if ((document.version !== 2 && document.version !== 3 && document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7) || !Array.isArray(need.components))
156
+ throw new Error(`volter init: --needs ${path} requires version 2,3,4,5,6 or7 for store components`);
157
+ return need.components.map((part) => {
158
+ if (!part || typeof part !== 'object' || Array.isArray(part))
159
+ throw new Error(`volter init: --needs ${path} has an invalid store component`);
160
+ const field = part;
161
+ if (typeof field.name !== 'string' || !['database', 'user', 'password'].includes(String(field.component)))
162
+ throw new Error(`volter init: --needs ${path} has an invalid store component`);
163
+ return { name: identifier(field.name, `the components of service ${String(need.service)}`), component: field.component };
164
+ });
165
+ })(),
166
+ // The variables that hold the service's port beside one that holds its host: keys of the environment file too.
167
+ ports: (Array.isArray(need.ports) ? need.ports : []).map((name) => identifier(String(name), `the port variables of service ${String(need.service)}`)),
168
+ portTargets: (() => {
169
+ if (need.portTargets === undefined)
170
+ return {};
171
+ if ((document.version !== 3 && document.version !== 4 && document.version !== 5 && document.version !== 6 && document.version !== 7) || !need.portTargets || typeof need.portTargets !== 'object' || Array.isArray(need.portTargets))
172
+ throw new Error(`volter init: --needs ${path} needs version3 for named vendor ports`);
173
+ return Object.fromEntries(Object.entries(need.portTargets).map(([name, value]) => {
174
+ if (!Array.isArray(need.ports) || !need.ports.includes(name) || !Number.isInteger(value) || Number(value) < 1 || Number(value) > 65535)
175
+ throw new Error(`volter init: --needs ${path} has an invalid named vendor port`);
176
+ return [identifier(name, 'named vendor ports'), Number(value)];
177
+ }));
178
+ })(),
179
+ })),
180
+ };
181
+ }
87
182
  // ---------------------------------------------------------------------------------------------
88
183
  // The pack catalog
89
184
  // ---------------------------------------------------------------------------------------------
@@ -278,6 +373,14 @@ const INFRA_PLACEHOLDER = {
278
373
  /** The compose recipes. Versions are CONSERVATIVE major pins (current LTS-grade, not latest):
279
374
  * a pilot wants "boots everywhere", not "newest features" — bump per pilot if the app demands. */
280
375
  const COMPOSE_INFRA = {
376
+ clickhouse: {
377
+ image: 'clickhouse/clickhouse-server:26.9.13.15',
378
+ containerPort: 8123,
379
+ // The existing managed-database Docker default; a backend limit, not measured adequacy.
380
+ memoryMiB: 1024,
381
+ volumePath: '/var/lib/clickhouse',
382
+ healthcheck: () => ['CMD', 'wget', '--quiet', '--output-document=-', 'http://127.0.0.1:8123/ping'],
383
+ },
281
384
  postgres: {
282
385
  image: 'postgres:16',
283
386
  containerPort: 5432,
@@ -299,8 +402,8 @@ const COMPOSE_INFRA = {
299
402
  volumePath: '/data',
300
403
  healthcheck: () => ['CMD', 'redis-cli', 'ping'],
301
404
  },
302
- // The runtime serves this kind with MongoDB's own mongod on PATH, without a container,
303
- // at the same loopback port and URL; command: ["mongod", "--replSet", "world"] enables transactions.
405
+ // The installed declaring twin, or MongoDB's own server, owns this kind's backing.
406
+ // A replica-set name is metadata passed to that backing, never a choice of executable.
304
407
  mongodb: {
305
408
  image: 'mongo:7',
306
409
  containerPort: 27017,
@@ -352,11 +455,14 @@ function composeService(name, kind, signals, taken) {
352
455
  // redis and mongodb run without authentication (no credentials in their images' env), so their URLs carry none
353
456
  const url = kind === 'redis'
354
457
  ? `redis://127.0.0.1:${hostPort}`
355
- : kind === 'mongodb'
356
- ? `mongodb://127.0.0.1:${hostPort}/${ident}`
357
- : `${kind}://${composeUser(name, kind)}:${ident}@127.0.0.1:${hostPort}/${ident}`;
458
+ : kind === 'clickhouse'
459
+ ? `http://${composeUser(name, kind)}:${ident}@127.0.0.1:${hostPort}/${ident}`
460
+ : kind === 'mongodb'
461
+ ? `mongodb://127.0.0.1:${hostPort}/${ident}`
462
+ : `${kind}://${composeUser(name, kind)}:${ident}@127.0.0.1:${hostPort}/${ident}`;
358
463
  return {
359
464
  kind,
465
+ ...(kind === 'clickhouse' ? { sides: { native: { hostPort: composePort(name, 'clickhouse-native', taken), containerPort: 9000 } }, configuration: { users: { default: '', [ident]: ident }, databases: [ident] } } : {}),
360
466
  image: recipe.image,
361
467
  hostPort,
362
468
  containerPort: recipe.containerPort,
@@ -372,11 +478,21 @@ function composeEnvironment(name, kind) {
372
478
  if (kind === 'postgres') {
373
479
  return [['POSTGRES_USER', ident], ['POSTGRES_PASSWORD', ident], ['POSTGRES_DB', ident]];
374
480
  }
481
+ if (kind === 'clickhouse')
482
+ return [['CLICKHOUSE_DB', ident], ['CLICKHOUSE_USER', ident], ['CLICKHOUSE_PASSWORD', ident], ['CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT', '1']];
375
483
  if (kind === 'mysql') {
376
484
  return [['MYSQL_ROOT_PASSWORD', ident], ['MYSQL_DATABASE', ident], ['MYSQL_USER', composeUser(name, kind)], ['MYSQL_PASSWORD', ident]];
377
485
  }
378
486
  return [];
379
487
  }
488
+ function managedPort(service, containerPort) {
489
+ if (containerPort === undefined || containerPort === service.containerPort)
490
+ return service.hostPort;
491
+ const side = Object.values(service.sides ?? {}).find(port => port.containerPort === containerPort);
492
+ if (!side)
493
+ throw new Error(`volter init: managed ${service.kind} does not serve vendor port ${containerPort}`);
494
+ return side.hostPort;
495
+ }
380
496
  /** Render the private managed-infrastructure definition — deterministic, with no absolute paths.
381
497
  * Only meaningful when `plan.compose` is non-empty (writeWorldInit skips it otherwise). */
382
498
  export function renderComposeFile(plan) {
@@ -390,13 +506,49 @@ export function renderComposeFile(plan) {
390
506
  for (const service of plan.compose) {
391
507
  const recipe = COMPOSE_INFRA[service.kind];
392
508
  lines.push(` ${service.kind}:`);
393
- lines.push(` image: ${service.image}`);
509
+ lines.push(` image: ${JSON.stringify(service.image)}`);
510
+ if (service.virtualTcpEndpoint)
511
+ lines.push(` x-volter-virtual-tcp-endpoint: ${JSON.stringify(service.virtualTcpEndpoint)}`);
512
+ if (service.replicaSet !== undefined)
513
+ lines.push(` x-volter-replica-set: ${JSON.stringify(service.replicaSet)}`);
514
+ if (service.nativeServerInputs) {
515
+ lines.push(' x-volter-interfaces: 3');
516
+ lines.push(` x-volter-native-server: ${JSON.stringify(nativeServerRuntimeReference(service.nativeServerInputs))}`);
517
+ }
518
+ if (service.originalInputs) {
519
+ if (!service.sides)
520
+ lines.push(' x-volter-interfaces: 2');
521
+ lines.push(` x-volter-original-inputs: ${JSON.stringify({ ...service.originalInputs, root: `original-inputs/${service.originalInputs.sha256}` })}`);
522
+ }
394
523
  lines.push(` mem_limit: ${service.memoryMiB}m`);
395
524
  lines.push(` memswap_limit: ${service.memoryMiB}m`);
396
525
  lines.push(' cpus: 1');
526
+ if (service.connectionBindings) {
527
+ lines.push(' x-volter-issued-dsn-bindings:');
528
+ for (const [name, binding] of Object.entries(service.connectionBindings)) {
529
+ lines.push(` ${name}:`);
530
+ for (const [field, value] of Object.entries(binding))
531
+ lines.push(` ${field}: ${JSON.stringify(value)}`);
532
+ }
533
+ }
397
534
  lines.push(' ports:');
398
535
  lines.push(` - "127.0.0.1:${service.hostPort}:${service.containerPort}"`);
536
+ for (const side of Object.values(service.sides ?? {}))
537
+ lines.push(` - "127.0.0.1:${side.hostPort}:${side.containerPort}"`);
538
+ if (service.sides) {
539
+ lines.push(` x-volter-interfaces: ${service.originalInputs ? 2 : 1}`);
540
+ lines.push(` x-volter-side-ports: ${JSON.stringify(service.sides)}`);
541
+ lines.push(` x-volter-configuration: ${JSON.stringify(service.configuration)}`);
542
+ }
399
543
  const environment = composeEnvironment(plan.name, service.kind);
544
+ if (service.nativeServerInputs)
545
+ for (const alias of service.nativeCredentialAliases ?? []) {
546
+ const canonical = alias.replace(/^MARIADB_/, 'MYSQL_');
547
+ const entry = environment.find(([name]) => name === canonical);
548
+ if (!entry)
549
+ throw new Error('volter init: native provider credential alias has no issued field');
550
+ environment.push([alias, entry[1]]);
551
+ }
400
552
  if (environment.length > 0) {
401
553
  lines.push(' environment:');
402
554
  for (const [key, value] of environment)
@@ -455,8 +607,11 @@ function repoEnvEntries(repo) {
455
607
  const path = resolve(dir, file);
456
608
  if (!existsSync(path))
457
609
  continue;
610
+ const text = sourceText(path);
611
+ if (text === undefined)
612
+ continue;
458
613
  const source = relative(repo, path) || file;
459
- for (const entry of parseEnvFile(readFileSync(path, 'utf8'), source)) {
614
+ for (const entry of parseEnvFile(text, source)) {
460
615
  if (inDir.has(entry.name))
461
616
  continue;
462
617
  inDir.add(entry.name);
@@ -497,7 +652,11 @@ function isInside(parent, candidate) {
497
652
  }
498
653
  export function planWorldInit(name, repoPath, options = {}) {
499
654
  assertSafeName(name);
655
+ // The World's own name, kept under a name no loop below takes: what it issues is made from it.
656
+ const worldName = name;
500
657
  const repo = resolve(repoPath);
658
+ // A World made from a stack's needs is made for an image: only what the project's source holds is read.
659
+ readOnlyWhatTheSourceHolds(options.needs ? repo : undefined);
501
660
  if (!existsSync(repo))
502
661
  throw new Error(`volter init: the application repo does not exist: ${repo}`);
503
662
  const root = resolve(options.root ?? repo);
@@ -519,7 +678,7 @@ export function planWorldInit(name, repoPath, options = {}) {
519
678
  /** a path the config carries: relative to the world root when the config lives under it
520
679
  * (portable between teammates), absolute otherwise (a pilot emitted away from its root) */
521
680
  const carried = (path) => (isInside(root, path) ? relative(root, path) : path);
522
- const catalogSource = resolveCatalog(root, { sources: options.sources, vendors: options.vendors });
681
+ const catalogSource = resolveCatalog(root, { sources: options.sources, vendors: options.vendors, mayBeEmpty: options.needs !== undefined });
523
682
  const catalog = new Set(catalogSource.vendors());
524
683
  // the catalog index's defaults (the installed `@volter/twin-catalog`), which name the twin to install for a vendor
525
684
  // this catalog lacks
@@ -528,7 +687,9 @@ export function planWorldInit(name, repoPath, options = {}) {
528
687
  // baked into the pack). Its handlers starter is the answering turns its own life scripts (journeys/scenario.json, the
529
688
  // World's handler format, its faults left out); its default data is src/semantics/seed.ts.
530
689
  const defaultsCopies = [];
531
- const signals = options.vendors ? null : detectRepoVendors(repo, existing?.services);
690
+ const stack = options.needs ? readStackNeeds(resolve(options.needs)) : undefined;
691
+ const stackEnvironmentNames = stack ? [...new Set([...stack.settings.map(entry => entry.name), ...stack.unset])].sort() : [];
692
+ const signals = options.vendors ? null : detectRepoVendors(repo, existing?.services, stackEnvironmentNames);
532
693
  const detected = options.vendors
533
694
  ? new Map(options.vendors.map((v) => [v, ['--twins']]))
534
695
  : new Map([...signals.detected].filter(([vendor]) => (signals.uses.get(vendor) ?? []).some((use) => worldSelectionIncludes(selection, vendor, use.usage))));
@@ -689,6 +850,36 @@ export function planWorldInit(name, repoPath, options = {}) {
689
850
  const mintedEnv = [];
690
851
  const infraSignals = new Map();
691
852
  const infraEntries = [];
853
+ // The project's own stack, where its image builder read it: each variable it wires to a service of a managed kind
854
+ // is that kind's signal whatever the variable is called, and is decided here before a name's pattern is asked.
855
+ const stackNames = new Map();
856
+ const stackNeeds = [];
857
+ /** The names actually given each service's address, filled where an entry is written: the report counts these. */
858
+ const stackWritten = new Map();
859
+ const kinds = (stack?.needs ?? []).map((need) => {
860
+ const fromScheme = need.names.map((entry) => entry.scheme === undefined ? null : schemeKindFor(`${entry.scheme}://`)).find((kind) => kind !== null) ?? null;
861
+ return { need, fromScheme, kind: fromScheme ?? (need.family !== undefined ? FAMILY_KIND[need.family] ?? null : null), source: `${stack.composeFile} service ${need.service}` };
862
+ });
863
+ // Explicit names first, over the whole stack: a service the stack wires by name keeps its names whatever is
864
+ // listed before it. Conventional names are for what is left, and never one another service was given.
865
+ for (const { need, kind, source } of kinds) {
866
+ if (kind === null)
867
+ continue;
868
+ for (const entry of need.names)
869
+ if (!stackNames.has(entry.name))
870
+ stackNames.set(entry.name, { kind, source, ...(entry.dsnFormat ? { dsnFormat: entry.dsnFormat, endpoint: entry.endpoint, dsnPrefix: entry.dsnPrefix, dsnProtocol: entry.dsnProtocol } : {}), host: entry.scheme === undefined, withPort: entry.withPort === true, ...(entry.scheme ? { scheme: entry.scheme } : {}), ...(entry.containerPort !== undefined ? { containerPort: entry.containerPort } : {}), ...(entry.query !== undefined ? { query: entry.query } : {}), service: need.service, conventional: false });
871
+ }
872
+ for (const { need, kind, source } of kinds) {
873
+ if (kind === null || need.names.length > 0)
874
+ continue;
875
+ for (const name of CONVENTIONAL_NAMES[kind] ?? [])
876
+ if (!stackNames.has(name))
877
+ stackNames.set(name, { kind, source, host: false, withPort: false, service: need.service, conventional: true });
878
+ }
879
+ const writeStackName = (name) => {
880
+ const wired = stackNames.get(name);
881
+ stackWritten.set(wired.service, [...(stackWritten.get(wired.service) ?? []), name]);
882
+ };
692
883
  // SMTP's host and port under the names this app declares: apps name them their own way (twenty's EMAIL_SMTP_HOST/
693
884
  // EMAIL_SMTP_PORT beside nodemailer's SMTP_HOST), and a raw-TCP twin is reached only through the env the app reads,
694
885
  // so every declared `…SMTP_HOST` / `…SMTP_PORT` points at the smtp twin (twenty lane: its mail went to the log). A World
@@ -717,6 +908,19 @@ export function planWorldInit(name, repoPath, options = {}) {
717
908
  ...(existing?.services ?? []),
718
909
  ...services.filter((service) => !existingIds.has(service.id)),
719
910
  ].flatMap(endpointNames));
911
+ const stackComponents = new Map();
912
+ const addressNames = new Set((stack?.needs ?? []).flatMap(need => [...need.names.map(entry => entry.name), ...need.ports]));
913
+ for (const { need, kind, source } of kinds)
914
+ for (const part of need.components) {
915
+ if (kind === null || addressNames.has(part.name) || worldInjected.has(part.name))
916
+ throw new Error(`volter init: ${part.name} has no exclusive managed store component binding`);
917
+ const held = stackComponents.get(part.name);
918
+ if (held && (held.kind !== kind || held.service !== need.service || held.component !== part.component))
919
+ throw new Error(`volter init: ${part.name} has conflicting managed store component bindings`);
920
+ stackComponents.set(part.name, { kind, source, service: need.service, component: part.component });
921
+ if (!held)
922
+ stackWritten.set(need.service, [...(stackWritten.get(need.service) ?? []), part.name]);
923
+ }
720
924
  // Every name a twin of this World issues (its pack's credentialDoor fill), by the twin: the pack names them, so a name
721
925
  // is issued whatever its stem (Vercel's TEAM_ID_VERCEL, Upstash's KV_REST_API_TOKEN) once
722
926
  // that vendor's twin is in the World, and never when it is not.
@@ -727,7 +931,18 @@ export function planWorldInit(name, repoPath, options = {}) {
727
931
  issuedBy.set(name, service.id);
728
932
  }
729
933
  const standInStems = new Set();
730
- for (const entry of repoEnvEntries(repo)) {
934
+ // THE STACK'S OWN STATEMENT OF THE APPLICATION'S ENVIRONMENT comes before the env files: the Compose file is what
935
+ // the project runs, an example env file what it suggests. Each name goes through the rules below as a name from a
936
+ // file does (a credential-shaped one is replaced by a fake or issued, never copied; an infrastructure one is a
937
+ // signal), so an application with no env file is still given its settings, and a secret the stack leaves to a
938
+ // variable nothing sets is one the World supplies.
939
+ const stackSource = stack ? `${stack.composeFile}${stack.application ? ` service ${stack.application}` : ''} environment` : '';
940
+ const stackEntries = stack ? [
941
+ ...stack.settings.map(({ name, value }) => ({ name, value, source: stackSource })),
942
+ ...stack.unset.filter((name) => !stack.settings.some((entry) => entry.name === name)).map((name) => ({ name, value: '', source: `${stackSource} (left to a variable nothing sets)` })),
943
+ ] : [];
944
+ const stated = new Set(stackEntries.map((entry) => entry.name));
945
+ for (const entry of [...stackEntries, ...repoEnvEntries(repo).filter((entry) => !stated.has(entry.name))]) {
731
946
  if (!envSources.includes(entry.source))
732
947
  envSources.push(entry.source);
733
948
  const { name, value, source, conflict } = entry;
@@ -800,12 +1015,33 @@ export function planWorldInit(name, repoPath, options = {}) {
800
1015
  envRows.push({ name, disposition: 'faked', source, reason: `${vendor} credential` });
801
1016
  continue;
802
1017
  }
803
- const infraKind = infraKindFor(name, value);
1018
+ // A conventional name is the stack's guess; where the application's own example gives that name a value whose
1019
+ // scheme says another kind (`DATABASE_URL=mysql://…` beside an unnamed PostgreSQL), the value is the evidence.
1020
+ const offered = stackNames.get(name);
1021
+ const valueKind = schemeKindFor(value);
1022
+ const wired = offered !== undefined && offered.conventional && valueKind !== null && valueKind !== offered.kind ? undefined : offered;
1023
+ if (offered !== undefined && wired === undefined)
1024
+ stackNames.delete(name);
1025
+ const infraKind = wired?.kind ?? infraKindFor(name, value);
804
1026
  if (infraKind !== null) {
1027
+ if (wired)
1028
+ writeStackName(name);
1029
+ let query = wired?.query;
1030
+ if (query === undefined && infraKind === 'mongodb') {
1031
+ const authority = /^mongodb(?:\+srv)?:\/\/([^/?#]+)/u.exec(value)?.[1]?.split('@').at(-1);
1032
+ if (authority?.includes(','))
1033
+ throw new Error(`volter init: ${name} names a multi-host MongoDB URI; one managed listener cannot retain that seed list`);
1034
+ try {
1035
+ const asked = new URL(value).searchParams.getAll('replicaSet');
1036
+ if (asked.length)
1037
+ query = new URLSearchParams(asked.map(set => ['replicaSet', set])).toString();
1038
+ }
1039
+ catch { /* a host or empty example declares no URI query */ }
1040
+ }
805
1041
  // DEFERRED: the value depends on whether this kind gets a compose service (a URL pointing
806
1042
  // at it) or stays a documented placeholder — decided below, once every signal is in.
807
1043
  infraSignals.set(infraKind, [...(infraSignals.get(infraKind) ?? []), name]);
808
- infraEntries.push({ name, source, kind: infraKind });
1044
+ infraEntries.push({ name, source: wired ? `${wired.source}; ${source}` : source, kind: infraKind, ...(wired?.dsnFormat ? { dsnFormat: wired.dsnFormat, endpoint: wired.endpoint, dsnPrefix: wired.dsnPrefix, dsnProtocol: wired.dsnProtocol } : {}), ...(wired?.scheme ? { scheme: wired.scheme } : {}), ...(wired?.containerPort !== undefined ? { containerPort: wired.containerPort } : {}), ...(query !== undefined ? { query } : {}), ...(wired?.host ? { host: true } : {}), ...(wired?.withPort ? { withPort: true } : {}) });
809
1045
  continue;
810
1046
  }
811
1047
  // A detected vendor's endpoint at a loopback server is the repository's local stand-in for the vendor (a garage for
@@ -820,8 +1056,16 @@ export function planWorldInit(name, repoPath, options = {}) {
820
1056
  // app-local secrets (JWT_SECRET, ENCRYPTION_KEY) and untwinned vendors alike — and it is the
821
1057
  // reason a live key committed to `.env.example` can never reach the emitted world.
822
1058
  if (isCredentialShapedEnvName(name) || isAppKeyName(name)) {
823
- env[name] = fakeEnvValue(name);
824
- envRows.push({ name, disposition: 'faked', source, reason: 'credential-shaped name — the example value is never copied' });
1059
+ // AN APPLICATION'S OWN SECRET IS ISSUED AT A GENERATOR'S LENGTH. A name no vendor claims and no shape answers
1060
+ // (AUTH_SECRET, JWT_SECRET, SESSION_SECRET) is the application's own, and applications check theirs: kaneo
1061
+ // refuses one under 32 characters, and `twin-fake-auth-secret` is 21. It is issued as 64 hexadecimal
1062
+ // characters, what `openssl rand -hex 32` gives, made from the World's name and the variable's so a World made
1063
+ // twice is the same World and two Worlds do not share a secret anyone could compute from the variable alone.
1064
+ // A vendor's credential keeps `twin-fake-…`, which its twin accepts and which reads as what it is.
1065
+ const generic = fakeEnvValue(name);
1066
+ const appsOwn = generic === `twin-fake-${name.toLowerCase().replace(/_/g, '-')}` && envNameVendor(name) === null;
1067
+ env[name] = appsOwn ? createHash('sha256').update(`${worldName}\0${generic}`).digest('hex') : generic;
1068
+ envRows.push({ name, disposition: 'faked', source, reason: appsOwn ? 'the application\'s own secret — issued as 64 hex characters, never the example value' : 'credential-shaped name — the example value is never copied' });
825
1069
  continue;
826
1070
  }
827
1071
  if (value !== '') {
@@ -879,6 +1123,33 @@ export function planWorldInit(name, repoPath, options = {}) {
879
1123
  row.reason = `the local stand-in's region, not one ${stem}'s vendor has — left unset`;
880
1124
  row.unset = true;
881
1125
  }
1126
+ // What the stack wires that no env file declared (an application with no env file at all), and what an env file
1127
+ // declared under a name no pattern knows, which was then kept or left as an ordinary value: the stack says it
1128
+ // names a service, so it takes the service's address.
1129
+ for (const [name, wired] of stackNames) {
1130
+ if (infraEntries.some((entry) => entry.name === name))
1131
+ continue;
1132
+ const held = envRows.findIndex((row) => row.name === name);
1133
+ if (held >= 0)
1134
+ envRows.splice(held, 1);
1135
+ delete env[name];
1136
+ infraSignals.set(wired.kind, [...(infraSignals.get(wired.kind) ?? []), name]);
1137
+ infraEntries.push({ name, source: wired.source, kind: wired.kind, ...(wired.dsnFormat ? { dsnFormat: wired.dsnFormat, endpoint: wired.endpoint, dsnPrefix: wired.dsnPrefix, dsnProtocol: wired.dsnProtocol } : {}), ...(wired.scheme ? { scheme: wired.scheme } : {}), ...(wired.containerPort !== undefined ? { containerPort: wired.containerPort } : {}), ...(wired.query !== undefined ? { query: wired.query } : {}), ...(wired.host ? { host: true } : {}), ...(wired.withPort ? { withPort: true } : {}) });
1138
+ writeStackName(name);
1139
+ }
1140
+ // What was made of each service, from what was WRITTEN: a service is named, or declared under conventional
1141
+ // names, only by the names that were given its address.
1142
+ for (const { need, kind, fromScheme } of kinds) {
1143
+ const base = { service: need.service, ...(need.image !== undefined ? { image: need.image } : {}), ...(need.family !== undefined ? { family: need.family } : {}) };
1144
+ const written = [...(stackWritten.get(need.service) ?? [])].sort();
1145
+ const conventional = kind !== null && need.names.length === 0;
1146
+ stackNeeds.push({ ...base, kind, names: written, conventional,
1147
+ note: kind === null ? (need.family === undefined ? 'an image of no family the World knows: nothing is declared for it' : `the World manages no ${need.family}: nothing is declared for it${need.names.length ? `, and ${need.names.map((entry) => entry.name).join(', ')} keep what init gives them` : ''}`)
1148
+ : written.length === 0 ? (conventional ? (CONVENTIONAL_NAMES[kind] ? 'not told by the definition which variable names it, and its conventional names were given to another service or carry a value of another kind: nothing is written for it' : `not told by the definition which variable names it, and ${kind} has no conventional name: nothing is written for it`)
1149
+ : `its names (${need.names.map((entry) => entry.name).join(', ')}) were given to another service of the stack first: nothing is written for it`)
1150
+ : conventional ? 'not told by the definition which variable names it; the conventional names are supplied'
1151
+ : `the kind is told by ${fromScheme !== null ? 'a value\'s scheme' : 'the service\'s image'}` });
1152
+ }
882
1153
  // A selected twin's declared database is part of the World's infrastructure, even when the application
883
1154
  // only reads that twin's issued connection URL. Do not substitute a guessed DATABASE_URL for its door.
884
1155
  const infrastructurePath = resolve(out, 'world.infrastructure.yml');
@@ -906,22 +1177,145 @@ export function planWorldInit(name, repoPath, options = {}) {
906
1177
  .sort(([a], [b]) => a.localeCompare(b))
907
1178
  .map(([kind, signals]) => composeService(name, kind, [...signals].sort(), takenPorts));
908
1179
  const composeByKind = new Map(compose.map((service) => [service.kind, service]));
1180
+ // A declared provider image remains the source stack's choice through endpoint allocation.
1181
+ const requestedImages = new Map();
1182
+ for (const need of stack?.needs ?? [])
1183
+ if (need.image) {
1184
+ const kind = need.family ? FAMILY_KIND[need.family] : undefined;
1185
+ const service = kind ? composeByKind.get(kind) : undefined;
1186
+ if (!service)
1187
+ continue;
1188
+ const prior = requestedImages.get(kind);
1189
+ if (prior && prior !== need.image)
1190
+ throw new Error(`volter init: distinct requested ${kind} images cannot share one managed backing`);
1191
+ requestedImages.set(kind, need.image);
1192
+ service.image = need.image;
1193
+ }
1194
+ for (const need of stack?.needs ?? [])
1195
+ if (need.originalInputs) {
1196
+ const kind = need.family ? FAMILY_KIND[need.family] : undefined;
1197
+ const service = kind ? composeByKind.get(kind) : undefined;
1198
+ if (!service)
1199
+ throw new Error(`volter init: original inputs for ${need.service} have no managed service`);
1200
+ if (service.originalInputs && service.originalInputs.sha256 !== need.originalInputs.sha256)
1201
+ throw new Error(`volter init: distinct original ${kind} service inputs cannot share one managed backing`);
1202
+ service.originalInputs = need.originalInputs;
1203
+ service.originalInputsFrom = dirname(resolve(options.needs));
1204
+ }
1205
+ for (const need of stack?.needs ?? [])
1206
+ if (need.nativeServerInputs) {
1207
+ const service = composeByKind.get('mysql');
1208
+ if (!service || need.family !== 'mysql')
1209
+ throw new Error('volter init: native MariaDB input has no managed MySQL service');
1210
+ if (service.originalInputs)
1211
+ throw new Error('volter init: a native provider cannot discard a separate original-input consumer requirement');
1212
+ if (service.nativeServerInputs && (service.nativeServerInputs.receiptSha256 !== need.nativeServerInputs.receiptSha256
1213
+ || service.nativeServerInputs.declarationSha256 !== need.nativeServerInputs.declarationSha256))
1214
+ throw new Error('volter init: distinct native MariaDB services cannot share one backing');
1215
+ service.nativeServerInputs = need.nativeServerInputs;
1216
+ service.nativeServerInputsFrom = dirname(resolve(options.needs));
1217
+ service.nativeCredentialAliases = need.nativeCredentialAliases ?? [];
1218
+ service.image = need.nativeServerInputs.image;
1219
+ service.volume = 'native-mariadb-data';
1220
+ }
1221
+ // The caller's MongoDB URI declares a server mode as well as connection options. Keep the raw query below,
1222
+ // and carry its set name to the backing instead of silently serving a standalone server at that address.
1223
+ const mongodb = composeByKind.get('mongodb');
1224
+ if (mongodb) {
1225
+ const sets = new Set();
1226
+ for (const entry of infraEntries.filter(entry => entry.kind === 'mongodb' && entry.query !== undefined)) {
1227
+ const asked = new URLSearchParams(entry.query).getAll('replicaSet');
1228
+ if (asked.length > 1 || asked.some(value => !/^[A-Za-z][A-Za-z0-9_-]*$/.test(value))) {
1229
+ throw new Error(`volter init: ${entry.name} must name one valid MongoDB replicaSet`);
1230
+ }
1231
+ for (const set of asked)
1232
+ sets.add(set);
1233
+ }
1234
+ if (sets.size > 1)
1235
+ throw new Error(`volter init: the managed MongoDB service cannot serve conflicting replicaSet names: ${[...sets].join(', ')}`);
1236
+ if (sets.size === 1)
1237
+ mongodb.replicaSet = [...sets][0];
1238
+ }
1239
+ const hostNamed = new Set(infraEntries.filter((entry) => entry.host).map((entry) => entry.name));
1240
+ // THE PORT BESIDE A HOST IS THE WORLD'S TO SAY. A stack that names a service by host and port apart (`REDIS_HOST:
1241
+ // redis`, `REDIS_PORT: 6379`) had its host pointed here and its port left at the stack's number, where nothing of
1242
+ // this World listens: the port is handed out per World, so two Worlds on one machine do not meet on 6379.
1243
+ for (const { need, kind } of kinds) {
1244
+ const composed = kind === null ? undefined : composeByKind.get(kind);
1245
+ if (composed === undefined)
1246
+ continue;
1247
+ for (const name of need.ports) {
1248
+ const held = envRows.findIndex((row) => row.name === name);
1249
+ if (held >= 0)
1250
+ envRows.splice(held, 1);
1251
+ const target = need.portTargets[name];
1252
+ if (composed.sides && target === undefined)
1253
+ throw new Error(`volter init: ${name} requires stack-needs version3 naming the vendor port for this multiwire service`);
1254
+ env[name] = String(managedPort(composed, target));
1255
+ envRows.push({ name, disposition: 'compose', source: `${stack.composeFile} service ${need.service}`, reason: `${kind} port — the World-managed infrastructure service's, in place of the stack's own number` });
1256
+ }
1257
+ }
909
1258
  for (const entry of infraEntries) {
910
1259
  const composed = composeByKind.get(entry.kind);
911
1260
  if (composed !== undefined) {
912
- env[entry.name] = composed.url;
1261
+ // A variable the stack set to a bare host (`REDIS_HOST: redis`) is a host, not a connection string.
1262
+ // The World owns the authority and database. The caller's raw options
1263
+ // remain byte-for-byte, including an explicitly empty query.
1264
+ if (composed.sides && entry.host && entry.withPort && entry.containerPort === undefined)
1265
+ throw new Error(`volter init: ${entry.name} requires stack-needs version3 naming the vendor port`);
1266
+ const target = entry.containerPort ?? (entry.kind === 'clickhouse' && entry.scheme === 'clickhouse' ? 9000 : undefined);
1267
+ const physicalPort = managedPort(composed, target);
1268
+ const port = entry.dsnFormat && entry.endpoint === 'default-port' ? composed.containerPort : physicalPort;
1269
+ if (entry.dsnFormat && entry.endpoint === 'default-port') {
1270
+ if (target !== composed.containerPort)
1271
+ throw new Error(`volter init: ${entry.name} default port differs from its declared listener`);
1272
+ composed.virtualTcpEndpoint ??= { version: 1, host: `w${fnv1a(name).toString(16)}-${composed.kind}.world.volter.internal`, port, targetPort: physicalPort };
1273
+ }
1274
+ // URL.protocol cannot change a special HTTP URL into the non-special native scheme.
1275
+ const connection = new URL(entry.kind === 'clickhouse' && port !== composed.hostPort
1276
+ ? composed.url.replace(/^http:/, 'clickhouse:') : composed.url);
1277
+ if (entry.dsnFormat && entry.endpoint === 'default-port')
1278
+ connection.hostname = composed.virtualTcpEndpoint.host;
1279
+ connection.port = String(port);
1280
+ if (entry.dsnFormat) {
1281
+ (composed.connectionBindings ??= {})[entry.name] = { dsnFormat: entry.dsnFormat, endpoint: entry.endpoint,
1282
+ dsnPrefix: entry.dsnPrefix, dsnProtocol: entry.dsnProtocol, containerPort: entry.containerPort, ...(entry.query !== undefined ? { query: entry.query } : {}) };
1283
+ }
1284
+ env[entry.name] = entry.dsnFormat ? renderGoMysqlBinding(entry, connection, port)
1285
+ : entry.host ? (entry.withPort ? `127.0.0.1:${port}` : '127.0.0.1')
1286
+ : `${entry.kind === 'clickhouse' ? connection.toString() : composed.url}${entry.query !== undefined ? `?${entry.query}` : ''}`;
913
1287
  envRows.push({
914
1288
  name: entry.name,
915
1289
  disposition: 'compose',
916
1290
  source: entry.source,
917
- reason: `${entry.kind} connection string — points at the World-managed infrastructure service`,
1291
+ reason: entry.dsnFormat ? 'mysql connection in its declared Go driver grammar — from the World-managed provider' : entry.host
1292
+ ? (entry.withPort ? `${entry.kind} host and port — the World-managed infrastructure service's` : `${entry.kind} host — the World-managed infrastructure service's (its port is ${composed.hostPort}; a port variable beside it is not set by this)`)
1293
+ : `${entry.kind} connection string — points at the World-managed infrastructure service`,
918
1294
  });
919
1295
  }
920
1296
  else {
1297
+ if (entry.dsnFormat)
1298
+ throw new Error(`volter init: ${entry.name} needs a supplied MySQL service for its declared Go driver grammar`);
921
1299
  env[entry.name] = INFRA_PLACEHOLDER[entry.kind] ?? 'REPLACE_ME';
922
1300
  envRows.push({ name: entry.name, disposition: 'placeholder', source: entry.source, reason: `${entry.kind} connection string — no twin models it; point it at your own local instance` });
923
1301
  }
924
1302
  }
1303
+ // A split field comes from the same connection as the host/URL. It is not
1304
+ // an endpoint signal: discovery must never replace a scalar with a URL.
1305
+ for (const [name, binding] of stackComponents) {
1306
+ const composed = composeByKind.get(binding.kind);
1307
+ if (!composed)
1308
+ throw new Error(`volter init: ${name} needs a managed ${binding.kind} connection for its ${binding.component}`);
1309
+ const url = new URL(composed.url);
1310
+ const held = envRows.findIndex(row => row.name === name);
1311
+ if (held >= 0)
1312
+ envRows.splice(held, 1);
1313
+ const value = decodeURIComponent(binding.component === 'database' ? url.pathname.slice(1) : binding.component === 'user' ? url.username : url.password);
1314
+ if (value === '')
1315
+ throw new Error(`volter init: ${name} asks for a ${binding.component} the managed ${binding.kind} connection does not provide`);
1316
+ env[name] = value;
1317
+ envRows.push({ name, disposition: 'compose', source: binding.source, reason: `${binding.kind} ${binding.component} — from the World-managed connection, beside its host and port` });
1318
+ }
925
1319
  const infra = [...infraSignals.entries()]
926
1320
  .filter(([kind]) => !composeByKind.has(kind))
927
1321
  .sort(([a], [b]) => a.localeCompare(b))
@@ -961,7 +1355,10 @@ export function planWorldInit(name, repoPath, options = {}) {
961
1355
  external: {
962
1356
  up: ['volter-world-infra', 'up'],
963
1357
  status: ['volter-world-infra', 'status'],
964
- discover: compose.flatMap((service) => service.signals.map((name) => ({ as: name, jsonPath: `connections.${name}`, ...(['postgres', 'mysql', 'mongodb', 'redis'].includes(service.kind) ? { protocol: service.kind } : {}) }))),
1358
+ // Only a connection string is discovered: status publishes the values that are a URL at the service's own
1359
+ // port (infraConnections), so a variable that holds a bare host (`REDIS_HOST: redis`, written above as
1360
+ // 127.0.0.1) is never among them, and asking for it made `up` fail on a World whose services were ready.
1361
+ discover: compose.flatMap((service) => service.signals.filter((name) => !hostNamed.has(name)).map((name) => ({ as: name, jsonPath: `connections.${name}`, ...(['postgres', 'mysql', 'mongodb', 'redis', 'clickhouse'].includes(service.kind) ? { protocol: service.kind === 'clickhouse' ? (env[name]?.startsWith('clickhouse:') ? 'clickhouse' : 'http') : service.kind } : {}) }))),
965
1362
  down: ['volter-world-infra', 'down'],
966
1363
  readyWhen: { command: 'volter-world-infra', args: ['status'], timeoutMs: 120000, intervalMs: 500 },
967
1364
  },
@@ -1058,6 +1455,7 @@ export function planWorldInit(name, repoPath, options = {}) {
1058
1455
  ? [] : projectMigrationTools(repo),
1059
1456
  infra,
1060
1457
  compose,
1458
+ ...(stack ? { stackNeeds, stackEnvironmentNames, leftAsThePersonsOwn: filesLeftAsThePersonsOwn() } : {}),
1061
1459
  composePath: compose.length > 0 ? resolve(out, 'world.infrastructure.yml') : null,
1062
1460
  // Standing registry acknowledgments leave the worklist but never vanish (each keeps its
1063
1461
  // recorded reason) — the same partition coverWorld's proof applies to the same signals.
@@ -1135,7 +1533,22 @@ export function writeWorldInit(plan, options = {}) {
1135
1533
  // Recheck the planned name-derived paths before any output is written.
1136
1534
  for (const copy of plan.defaultsCopies)
1137
1535
  ownedPath(plan.out, relative(plan.out, copy.to));
1536
+ for (const service of plan.compose)
1537
+ if (service.nativeServerInputs)
1538
+ ownedPath(plan.out, `native-inputs/${service.nativeServerInputs.receiptSha256}/${service.nativeServerInputs.declarationSha256}`);
1138
1539
  mkdirSync(plan.out, { recursive: true });
1540
+ for (const service of plan.compose)
1541
+ if (service.nativeServerInputs) {
1542
+ if (!service.nativeServerInputsFrom)
1543
+ throw new Error('Native managed input has no retained source parent');
1544
+ copyNativeServerInputs(service.nativeServerInputsFrom, service.nativeServerInputs, plan.out);
1545
+ }
1546
+ for (const service of plan.compose)
1547
+ if (service.originalInputs) {
1548
+ if (!service.originalInputsFrom)
1549
+ throw new Error('Original inputs have no retained source parent');
1550
+ copyOriginalInputSnapshot(service.originalInputsFrom, service.originalInputs, plan.out);
1551
+ }
1139
1552
  writeFileSync(plan.configPath, `${JSON.stringify(worldConfigDocument(plan.config), null, 2)}\n`);
1140
1553
  if (plan.inRepo)
1141
1554
  writeFileSync(join(plan.out, '.gitignore'), STATE_GITIGNORE);
@@ -1253,6 +1666,7 @@ export function initWorld(name, repoPath, options = {}) {
1253
1666
  root: resolve(options.root ?? process.cwd()),
1254
1667
  allowUnknown: options.allowUnknown ?? false,
1255
1668
  acknowledge: options.acknowledge ?? {},
1669
+ ...(plan.stackEnvironmentNames ? { declaredEnvironmentNames: plan.stackEnvironmentNames } : {}),
1256
1670
  });
1257
1671
  // The proof was addressed BY PATH (so it reads the bytes just written, not the in-memory plan);
1258
1672
  // report it under the world's id, which is what every other verb takes.
@@ -1342,6 +1756,19 @@ export function formatInitReport(result) {
1342
1756
  }
1343
1757
  lines.push(' Resolve these before expecting the app to listen. Vendor coverage can be green while the app rejects its own env.');
1344
1758
  }
1759
+ if (plan.stackNeeds !== undefined) {
1760
+ const told = (test) => plan.stackNeeds.filter(test).length;
1761
+ lines.push('');
1762
+ lines.push('READ FOR AN IMAGE: only files the project\'s source holds were read (--needs).');
1763
+ for (const file of plan.leftAsThePersonsOwn ?? [])
1764
+ lines.push(` ${file} is the person's own and was not read`);
1765
+ lines.push(`THE PROJECT'S OWN STACK (${plan.stackNeeds.length} service${plan.stackNeeds.length === 1 ? '' : 's'} beside the application): `
1766
+ + `${told((need) => need.kind !== null && !need.conventional && need.names.length > 0)} given their address under the stack's own variables, ${told((need) => need.conventional && need.names.length > 0)} under conventional names, `
1767
+ + `${told((need) => need.kind !== null && need.names.length === 0)} of a managed kind with nothing written, ${told((need) => need.kind === null)} the World has no kind for`);
1768
+ for (const need of plan.stackNeeds) {
1769
+ lines.push(` ${need.service}${need.image ? ` (${need.image})` : ''}: ${need.kind ?? 'no managed kind'}${need.names.length ? ` — ${need.names.join(', ')}` : ''}; ${need.note}`);
1770
+ }
1771
+ }
1345
1772
  if (plan.undeclaredMigrations.length > 0) {
1346
1773
  lines.push('');
1347
1774
  lines.push(`MIGRATION TOOL FOUND, NO STEP DECLARED (${plan.undeclaredMigrations.length}):`);