@volter/world-runtime 2.0.55 → 3.0.0

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 (121) hide show
  1. package/dist/known-external-services.json +5 -4
  2. package/dist/src/actions-runner-cli.d.ts +2 -0
  3. package/dist/src/actions-runner-cli.js +25 -0
  4. package/dist/src/actions-runner.d.ts +18 -0
  5. package/dist/src/actions-runner.js +281 -0
  6. package/dist/src/ancestry.js +1 -1
  7. package/dist/src/app-url.js +1 -1
  8. package/dist/src/backings.d.ts +29 -0
  9. package/dist/src/backings.js +45 -0
  10. package/dist/src/branch.js +3 -2
  11. package/dist/src/browser-proxy-cli.js +1 -1
  12. package/dist/src/catalog-index.d.ts +38 -0
  13. package/dist/src/catalog-index.js +37 -0
  14. package/dist/src/catalog.d.ts +16 -3
  15. package/dist/src/catalog.js +69 -26
  16. package/dist/src/changeset.d.ts +1 -1
  17. package/dist/src/changeset.js +9 -11
  18. package/dist/src/cli-args.d.ts +4 -0
  19. package/dist/src/cli-args.js +18 -0
  20. package/dist/src/cli-commands.d.ts +1 -0
  21. package/dist/src/cli-commands.js +1216 -0
  22. package/dist/src/cli.js +8 -1300
  23. package/dist/src/configs.js +1 -1
  24. package/dist/src/console-apart.d.ts +2 -2
  25. package/dist/src/console-apart.js +5 -5
  26. package/dist/src/consumers.js +1 -1
  27. package/dist/src/covers.d.ts +1 -1
  28. package/dist/src/covers.js +94 -30
  29. package/dist/src/env-output.d.ts +1 -1
  30. package/dist/src/env-output.js +2 -2
  31. package/dist/src/fixture-env.js +18 -26
  32. package/dist/src/host-cli.js +4 -2
  33. package/dist/src/host-fault-fixture.js +6 -0
  34. package/dist/src/host-ready.d.ts +8 -2
  35. package/dist/src/host-ready.js +16 -4
  36. package/dist/src/host-worker.js +6 -1
  37. package/dist/src/host.d.ts +2 -0
  38. package/dist/src/host.js +9 -1
  39. package/dist/src/index.d.ts +2 -2
  40. package/dist/src/index.js +2 -2
  41. package/dist/src/init.d.ts +33 -23
  42. package/dist/src/init.js +228 -96
  43. package/dist/src/lifecycle-record.js +2 -2
  44. package/dist/src/local-branches.js +1 -1
  45. package/dist/src/origin.d.ts +1 -1
  46. package/dist/src/origin.js +12 -10
  47. package/dist/src/pack-facts.d.ts +30 -6
  48. package/dist/src/pack-facts.js +13 -3
  49. package/dist/src/pglite-backing.js +2 -1
  50. package/dist/src/placeholder.d.ts +1 -1
  51. package/dist/src/placeholder.js +12 -4
  52. package/dist/src/project-inspect.d.ts +1 -1
  53. package/dist/src/project-inspect.js +2 -2
  54. package/dist/src/redirect-proxy.d.ts +5 -0
  55. package/dist/src/redirect-proxy.js +8 -1
  56. package/dist/src/redis-backing.js +4 -2
  57. package/dist/src/reflect.js +1 -1
  58. package/dist/src/root.d.ts +11 -33
  59. package/dist/src/root.js +28 -59
  60. package/dist/src/runtime-test-support.js +1 -1
  61. package/dist/src/runtime.d.ts +7 -1
  62. package/dist/src/runtime.js +317 -53
  63. package/dist/src/schema.d.ts +27 -5
  64. package/dist/src/schema.js +13 -12
  65. package/dist/src/served-world.d.ts +11 -35
  66. package/dist/src/served-world.js +54 -141
  67. package/dist/src/tail.js +1 -1
  68. package/dist/src/up-cli.d.ts +3 -0
  69. package/dist/src/up-cli.js +55 -0
  70. package/dist/src/websocket-relay.d.ts +1 -1
  71. package/dist/src/world-origins.d.ts +1 -1
  72. package/dist/src/world-origins.js +1 -1
  73. package/dist/src/world-view.d.ts +1 -1
  74. package/dist/src/world-view.js +2 -2
  75. package/known-external-services.json +5 -4
  76. package/package.json +11 -11
  77. package/src/actions-runner-cli.ts +24 -0
  78. package/src/actions-runner.ts +244 -0
  79. package/src/ancestry.ts +1 -1
  80. package/src/app-url.ts +1 -1
  81. package/src/backings.ts +55 -0
  82. package/src/branch.ts +3 -2
  83. package/src/browser-proxy-cli.ts +1 -1
  84. package/src/catalog-index.ts +44 -0
  85. package/src/catalog.ts +67 -24
  86. package/src/changeset.ts +10 -13
  87. package/src/cli-args.ts +17 -0
  88. package/src/cli-commands.ts +1070 -0
  89. package/src/cli.ts +9 -1139
  90. package/src/configs.ts +1 -1
  91. package/src/console-apart.ts +5 -5
  92. package/src/consumers.ts +1 -1
  93. package/src/covers.ts +83 -30
  94. package/src/env-output.ts +2 -2
  95. package/src/fixture-env.ts +18 -26
  96. package/src/host-cli.ts +4 -2
  97. package/src/host-ready.ts +13 -4
  98. package/src/host-worker.ts +6 -2
  99. package/src/host.ts +11 -2
  100. package/src/index.ts +2 -2
  101. package/src/init.ts +222 -101
  102. package/src/lifecycle-record.ts +2 -2
  103. package/src/local-branches.ts +1 -1
  104. package/src/origin.ts +12 -10
  105. package/src/pack-facts.ts +22 -6
  106. package/src/pglite-backing.ts +2 -1
  107. package/src/placeholder.ts +13 -4
  108. package/src/project-inspect.ts +2 -2
  109. package/src/redirect-proxy.ts +9 -1
  110. package/src/redis-backing.ts +4 -2
  111. package/src/reflect.ts +1 -1
  112. package/src/root.ts +33 -65
  113. package/src/runtime-test-support.ts +1 -1
  114. package/src/runtime.ts +261 -55
  115. package/src/schema.ts +26 -13
  116. package/src/served-world.ts +50 -130
  117. package/src/tail.ts +1 -1
  118. package/src/up-cli.ts +55 -0
  119. package/src/websocket-relay.ts +1 -1
  120. package/src/world-origins.ts +1 -1
  121. package/src/world-view.ts +3 -3
package/src/init.ts CHANGED
@@ -34,7 +34,7 @@
34
34
  // example value read by production source can block before listen even when vendor coverage is
35
35
  // green. `init` never boots the world: it diagnoses, the operator (or the room) boots.
36
36
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
37
- import { PROTOCOL_MAJOR, stateDirName } from '@volter/world-core';
37
+ import { PROTOCOL_MAJOR, stateDirName } from '@volter/world-core/runtime';
38
38
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
39
39
  import {
40
40
  coverWorld,
@@ -48,12 +48,23 @@ import {
48
48
  type CoverageReport,
49
49
  } from './covers.ts';
50
50
  import { fakeEnvValue, isAppKeyName, isGoogleOAuthClientEnvName, isGoogleServiceAccountEnvName } from './fixture-env.ts';
51
- import { resolveCatalog, twinPackageName, type Catalog } from './catalog.ts';
51
+ import { findInstalledPackage, packCli, packEntry, resolveCatalog, twinPackageName, type Catalog } from './catalog.ts';
52
+ import { CATALOG_INDEX_PACKAGE, defaults as catalogDefaults } from './catalog-index.ts';
52
53
  import { projectEnvReads, projectManifestDirs } from './project-inspect.ts';
53
54
  import { overlayEndpointEnv, packFacts } from './pack-facts.ts';
54
- import { DEFAULT_WORLD_SELECTION, MINT_ENV, worldConfigDocument, worldSelectionIncludes, type WorldConfig, type WorldSelection, type WorldServiceConfig } from './schema.ts';
55
+ import { VENDOR_SERVER_BACKINGS } from './backings.ts';
56
+ import { APP_URL_ENV, DEFAULT_WORLD_SELECTION, ISSUE_ENV_PREFIX, MINT_ENV, worldConfigDocument, worldSelectionIncludes, type WorldConfig, type WorldSelection, type WorldServiceConfig } from './schema.ts';
55
57
  import { loadWorldConfig } from './configs.ts';
56
58
 
59
+ /** The names a pack's credential door fills with a file it writes (`file:<field>`, the descriptor's): a path, never a value. */
60
+ function fileCredentialNames(): Set<string> {
61
+ const out = new Set<string>();
62
+ try {
63
+ for (const facts of Object.values(packFacts())) for (const [name, spec] of Object.entries(facts?.credentialDoor?.fill ?? {})) if (String(spec).startsWith('file:')) out.add(name);
64
+ } catch { /* no artifact: no file doors known */ }
65
+ return out;
66
+ }
67
+
57
68
  /** How a twin service in the emitted world is reached by the app's traffic. */
58
69
  export type InitWiring =
59
70
  /** the injector redirects the vendor's real hosts; `injectEnv` is a var it genuinely reads */
@@ -62,7 +73,9 @@ export type InitWiring =
62
73
  | 'app-read'
63
74
  /** no injector entry and no endpoint env: the SDK is pointed at the twin by explicit client config */
64
75
  | 'explicit-config'
65
- /** detected vendor with no `packages/twin/<vendor>` pack in the catalog — nothing to emit */
76
+ /** the vendor's own open-source server runs as the World's service (VENDOR_SERVER_BACKINGS) */
77
+ | 'backing'
78
+ /** detected vendor with no vendor pack in the catalog — nothing to emit */
66
79
  | 'no-pack';
67
80
 
68
81
  export type InitVendorPlan = {
@@ -76,18 +89,47 @@ export type InitVendorPlan = {
76
89
  service: WorldServiceConfig | null;
77
90
  /** why this wiring — printed in the report and embedded in the service's `//` note */
78
91
  note: string;
92
+ /** the source init chose the pack from (architecture, "The catalog: where twins come from"): where it comes from, its
93
+ * protocol and its version */
94
+ source?: { kind: 'checkout' | 'installed'; version?: string };
79
95
  };
80
96
 
97
+ /** The env stem of a detected vendor whose endpoint this is, when it points at a loopback server; otherwise null. */
98
+ function standInStem(name: string, value: string, detected: ReadonlyMap<string, unknown>): string | null {
99
+ // an endpoint override only (`S3_ENDPOINT`, `AWS_ENDPOINT_URL`): a vendor's `*_URL` may be its only address (Supabase's
100
+ // project URL) or the app's own (`STRIPE_WEBHOOK_URL`), never a stand-in to drop
101
+ if (!/(^|_)(ENDPOINT|ENDPOINT_URL)$/.test(name) || !/^https?:\/\//.test(value)) return null;
102
+ let host: string;
103
+ try { host = new URL(value).hostname.replace(/^\[|\]$/g, ''); } catch { return null; }
104
+ if (!['localhost', '127.0.0.1', '::1', '0.0.0.0', 'host.docker.internal'].includes(host)) return null;
105
+ for (const vendor of detected.keys()) {
106
+ for (const stem of packFacts()[vendor]?.adoption?.envStems ?? []) {
107
+ if (name.startsWith(`${stem}_`)) return stem;
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+
113
+ /** Names that carry the application's own origin: its base URL (Next's public one included) and its auth library's. */
114
+ const APP_ORIGIN_ENV = /^(?:NEXT_PUBLIC_)?(?:APP_)?(?:BASE_)?URL$|^NEXT_PUBLIC_APP_(?:BASE_)?URL$|^(?:NEXTAUTH|BETTER_AUTH|AUTH)_URL$/;
115
+
81
116
  /** What `init` did with one env NAME found in the repo's example env files. */
82
117
  export type EnvDisposition =
83
118
  /** credential-shaped → replaced by a structurally valid fake (never the committed value) */
84
119
  | 'faked'
120
+ /** a credential the vendor's twin issues at boot → `$issue:<vendor>` (the twin refuses a key it did not make) */
121
+ | 'issued'
85
122
  /** non-credential app config → the example value copied verbatim */
86
123
  | 'kept'
87
124
  /** infra connection string → points at a World-managed infrastructure service */
88
125
  | 'compose'
89
126
  /** infra connection string → a documented placeholder the operator fills in */
90
127
  | 'placeholder'
128
+ /** the application's own origin → `$app-url`, the address its booter records, resolved when it attaches */
129
+ | 'app-origin'
130
+ /** a detected vendor's endpoint pointed at a local stand-in server (a garage or MinIO for S3) → left unset, so the SDK
131
+ * addresses the vendor's own host, which the World routes to its twin */
132
+ | 'stand-in'
91
133
  /** neither credential-shaped nor valued: the operator has to decide */
92
134
  | 'unknown';
93
135
 
@@ -172,7 +214,8 @@ export type InitPlan = {
172
214
  acknowledgedExternal: Array<{ name: string; reason: string }>;
173
215
  /** pack `defaults/` files copied into the world dir (TWIN-PROGRAMMING-MODEL: default
174
216
  * reasonable — visible starter seeds/handlers, ownership transfers on copy). */
175
- defaultsCopies: Array<{ vendor: string; kind: 'handlers' | 'seed'; from: string; to: string }>;
217
+ /** `from` is copied byte for byte; `content` (a runner init writes for a protocol-3 pack's seed data) is written as given. */
218
+ defaultsCopies: Array<{ vendor: string; kind: 'handlers' | 'seed' | 'seed-data'; from?: string; content?: string; to: string }>;
176
219
  /** emitted when any pack ships a default seed: the one explicit entry point + story skeleton */
177
220
  seedEntry: { entryPath: string; storyPath: string } | null;
178
221
  /** Explicit wiring for new default seeds when the existing entry point is operator-owned. */
@@ -206,7 +249,7 @@ export type InitResult = {
206
249
 
207
250
  export type InitOptions = {
208
251
  /** the WORLD root: where the emitted config boots from, and where the catalog is resolved —
209
- * this checkout's `packages/twin` when init runs here, else the `@volter/twin-<vendor>`
252
+ * the sibling pack repository when init runs here, else the `@volter/twin-<vendor>`
210
253
  * packages installed above it (docs/concepts/worlds.md#the-config-and-the-running-world). Defaults to the
211
254
  * application repo. */
212
255
  root?: string;
@@ -226,7 +269,7 @@ export type InitOptions = {
226
269
  // ---------------------------------------------------------------------------------------------
227
270
 
228
271
  /** The twins available at `root`, sorted — a deterministic input (catalog.ts: this checkout's
229
- * `packages/twin/<vendor>/` dirs, or the `@volter/twin-<vendor>` packages installed above root). */
272
+ * vendor directories in the pack repository, or the `@volter/twin-<vendor>` packages installed above root). */
230
273
  export function packCatalog(root: string): string[] {
231
274
  return resolveCatalog(root).vendors();
232
275
  }
@@ -236,38 +279,13 @@ export function packCatalog(root: string): string[] {
236
279
  // ---------------------------------------------------------------------------------------------
237
280
 
238
281
  /**
239
- * The REAL endpoint env names for packs the injector cannot intercept — the other half of
240
- * `scripts/vendor-hosts.test.ts`'s descriptor hostsNone ruling. That allowlist states WHY each pack
241
- * has no VENDOR_HOSTS entry; this table states WHAT to wire instead, using the env var the vendor's
242
- * own SDK documents. Every entry is grounded in the SDK, not invented:
243
- *
244
- * inngest `INNGEST_BASE_URL` (+ `INNGEST_EVENT_API_BASE_URL` for the event API)
245
- * — inngest@4's own apiBaseUrl/eventBaseUrl getters.
246
- * ai-gateway `AI_GATEWAY_BASE_URL` — `@ai-sdk/gateway`'s baseURL env.
247
- * livekit `LIVEKIT_URL` as a ws:// template — the media plane is a WebSocket
248
- * URL the SDK takes as its first constructor argument.
249
- *
250
- * A pack that is base-URL-configurable but has NO conventional env var (algolia, pinecone,
251
- * replicate, fal, twilio, sendblue, figma, notion) is deliberately ABSENT: it gets a service
252
- * with no endpoint env at all, and the proof reports it UNINTERCEPTABLE. Inventing a
253
- * `<VENDOR>_BASE_URL` for it would make `covers` call the world covered (any app-read env counts)
254
- * while the app, which reads no such var, still talks to the real vendor — the exact class of lie
255
- * the proof exists to catch.
282
+ * The endpoint env for a pack the injector cannot intercept and whose descriptor cannot state it as `endpointEnv`
283
+ * (which receives the twin's URL): a raw protocol, whose client is pointed by host and port. Every name is one a real
284
+ * client documents. A pack with no such env is absent: it gets a service with no endpoint env at all, and the proof
285
+ * reports it UNINTERCEPTABLE. Inventing a `<VENDOR>_BASE_URL` for it would make `covers` call the world covered while
286
+ * the app, which reads no such var, still talks to the real vendor.
256
287
  */
257
288
  export const APP_READ_ENDPOINT_ENV: Record<string, { injectEnv?: string; injectEnvTemplates?: Record<string, string>; note: string }> = {
258
- tunnel: {
259
- injectEnv: 'TUNNEL_SERVER_URL',
260
- note: 'no injector entry: the native volter-tunnel CLI and @volter/tunnel WebSocket client are endpoint-configured through TUNNEL_SERVER_URL / the SDK host option; native cloudflared is outside Node HTTP injection.',
261
- },
262
- // (inngest moved to its pack descriptor's `endpointEnv` — the descriptor-first exemplar (adding-a-twin.md §3).)
263
- 'ai-gateway': {
264
- injectEnv: 'AI_GATEWAY_BASE_URL',
265
- note: 'no injector entry: @ai-sdk/gateway is base-URL-configured through AI_GATEWAY_BASE_URL.',
266
- },
267
- livekit: {
268
- injectEnvTemplates: { LIVEKIT_URL: 'ws://${host}:${port}' },
269
- note: 'no injector entry: LIVEKIT_URL is a ws:// endpoint the SDK takes directly — WebSocket media is outside the HTTP injector\'s reach.',
270
- },
271
289
  // RAW PROTOCOL. Not "no injector entry yet" — there is no host to map at all: SMTP is a line
272
290
  // protocol over a raw TCP socket, so the injector's http/fetch patches never see it, and the
273
291
  // relay hostname is whatever the operator configured rather than a fixed vendor host. The env
@@ -288,30 +306,25 @@ export const APP_READ_ENDPOINT_ENV: Record<string, { injectEnv?: string; injectE
288
306
  note: 'no injector entry and none possible: SMTP is a RAW TCP protocol, invisible to the http/fetch injector, '
289
307
  + 'with no fixed vendor host. Interception is the app-read env itself — point SMTP_HOST/SMTP_PORT (or Cal.com\'s '
290
308
  + 'EMAIL_SERVER_HOST/PORT, or NextAuth\'s EMAIL_SERVER url) at the twin\'s listener and an unmodified nodemailer '
291
- + 'sends there. Read the mail back over the twin-only inspect sidecar: `world-smtp serve --inspect-port N`, then '
292
- + 'GET http://127.0.0.1:N/twin/messages/latest.',
293
- },
294
- // RAW PROTOCOL, the gRPC kind. The Temporal SDKs dial the frontend address they are given
295
- // (`Connection.connect({ address })`, `NativeConnection.connect({ address })`), conventionally read
296
- // from TEMPORAL_ADDRESS — Postiz's temporal.module.ts, the SDK samples and the Temporal CLI all read
297
- // that name — and speak gRPC over HTTP/2, which the http/fetch injector never sees. The value is an
298
- // address (host:port), not a URL, which is why this lives here as a template and not as the
299
- // descriptor's `endpointEnv` (whose `name` receives the twin's URL).
300
- temporal: {
301
- injectEnvTemplates: {
302
- TEMPORAL_ADDRESS: '${host}:${port}',
303
- },
304
- note: 'no injector entry and none possible: the Temporal SDKs speak gRPC to the address they are configured with. '
305
- + 'TEMPORAL_ADDRESS=<host>:<port> points them at the twin (namespace `default` exists); an app that sets '
306
- + 'TEMPORAL_API_KEY turns TLS on in the SDK, which this loopback frontend does not speak — leave it unset.',
309
+ + 'sends there. Read the mail back at the mailbox the World records beside it: `volter-world url <world> smtp.inspect` '
310
+ + '(/_twin/messages?to=<address>, newest first; /_twin/messages/<id> for one).',
307
311
  },
308
312
  };
309
- // descriptor-first migration (adding-a-twin.md §3): packs now declare their endpoint-env wiring (with its grounding
313
+ export { VENDOR_SERVER_BACKINGS } from './backings.ts';
314
+
315
+ // descriptor-first migration (architecture.md, "The descriptor"): packs now declare their endpoint-env wiring (with its grounding
310
316
  // note) on the descriptor (`endpointEnv` on TwinPack); the table above shrinks toward empty as
311
317
  // entries move. The overlay throws on a vendor declared in both homes.
312
318
  overlayEndpointEnv(APP_READ_ENDPOINT_ENV);
313
319
 
314
320
  /** The canonical wiring for one vendor's twin service, derived from the injector where possible. */
321
+ /** A twin service with its pack's credential door (`issues`) as the pack declares it now: a service kept from a saved
322
+ * config gets the current door, so its names are issued by the door the pack has, never by a copy it has since fixed. */
323
+ function withIssues(service: WorldServiceConfig): WorldServiceConfig {
324
+ const door = service.type === 'twin' ? packFacts()[service.id]?.credentialDoor : undefined;
325
+ return door === undefined ? service : { ...service, issues: door };
326
+ }
327
+
315
328
  function wiringFor(vendor: string): {
316
329
  wiring: Exclude<InitWiring, 'no-pack'>;
317
330
  injectEnv?: string;
@@ -320,7 +333,7 @@ function wiringFor(vendor: string): {
320
333
  serviceEnv?: Record<string, string>;
321
334
  note: string;
322
335
  } {
323
- // RULE 2: ask the injector, never guess. `aws` yields s3/dynamodb/timestream here — the very keys
336
+ // RULE 2: ask the injector, never guess. `aws` yields s3/secretsmanager here — the very keys
324
337
  // the inert `AWS_TWIN_URL` does not.
325
338
  const keys = injectorVendorKeysFor(vendor);
326
339
  const twinUrlNames = keys.map(injectorEnvNameForKey);
@@ -607,7 +620,7 @@ export function renderComposeFile(plan: InitPlan): string {
607
620
  return `${lines.join('\n')}\n`;
608
621
  }
609
622
 
610
- export type ParsedEnvEntry = { name: string; value: string; source: string };
623
+ export type ParsedEnvEntry = { name: string; value: string; source: string; /** several dirs define it differently: each `source=value` */ conflict?: string[] };
611
624
 
612
625
  /** Parse one dotenv-ish file into ordered NAME/value pairs. Deliberately conservative: the same
613
626
  * `NAME=` line shape `projectEnvNames` detects, plus quote stripping, so the two views of a repo's
@@ -634,23 +647,36 @@ export function parseEnvFile(text: string, source: string): ParsedEnvEntry[] {
634
647
  return entries;
635
648
  }
636
649
 
637
- /** Every example-env entry in the repo, first definition winning, in a deterministic order:
638
- * root before workspace members (sorted), and within a dir the ENV_SOURCE_FILES priority. */
650
+ /** Every example-env entry in the repo, in a deterministic order: root before workspace members (sorted), and within a
651
+ * dir the ENV_SOURCE_FILES priority. A name defined in several dirs with one value is that value; with different values
652
+ * it is the definition of the dir that holds the application (its Prisma schema or its vercel.json) when exactly one such
653
+ * dir defines it, and otherwise a conflict the operator decides, each definition listed (a landing page's base URL must
654
+ * never silently become the app's). */
639
655
  function repoEnvEntries(repo: string): ParsedEnvEntry[] {
640
- const seen = new Set<string>();
641
- const entries: ParsedEnvEntry[] = [];
656
+ const byName = new Map<string, Array<ParsedEnvEntry & { dir: string }>>();
642
657
  for (const dir of projectManifestDirs(repo)) {
658
+ const inDir = new Set<string>();
643
659
  for (const file of ENV_SOURCE_FILES) {
644
660
  const path = resolve(dir, file);
645
661
  if (!existsSync(path)) continue;
646
662
  const source = relative(repo, path) || file;
647
663
  for (const entry of parseEnvFile(readFileSync(path, 'utf8'), source)) {
648
- if (seen.has(entry.name)) continue;
649
- seen.add(entry.name);
650
- entries.push(entry);
664
+ if (inDir.has(entry.name)) continue;
665
+ inDir.add(entry.name);
666
+ byName.set(entry.name, [...(byName.get(entry.name) ?? []), { ...entry, dir }]);
651
667
  }
652
668
  }
653
669
  }
670
+ const holdsApp = (dir: string): boolean => existsSync(resolve(dir, 'prisma', 'schema.prisma')) || existsSync(resolve(dir, 'vercel.json'));
671
+ const entries: ParsedEnvEntry[] = [];
672
+ for (const defs of byName.values()) {
673
+ const first = defs[0]!;
674
+ const values = new Set(defs.map((d) => d.value).filter((v) => v !== ''));
675
+ if (values.size <= 1) { entries.push({ name: first.name, value: defs.find((d) => d.value !== '')?.value ?? '', source: defs.find((d) => d.value !== '')?.source ?? first.source }); continue; }
676
+ const app = defs.filter((d) => d.value !== '' && holdsApp(d.dir));
677
+ if (app.length === 1) { entries.push({ name: first.name, value: app[0]!.value, source: app[0]!.source }); continue; }
678
+ entries.push({ name: first.name, value: first.value, source: first.source, conflict: defs.filter((d) => d.value !== '').map((d) => `${d.source}=${d.value}`) });
679
+ }
654
680
  return entries;
655
681
  }
656
682
 
@@ -696,9 +722,13 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
696
722
 
697
723
  const catalogSource: Catalog = resolveCatalog(root);
698
724
  const catalog = new Set(catalogSource.vendors());
725
+ // the catalog index's defaults (the installed `@volter/twin-catalog`), which name the twin to install for a vendor
726
+ // this catalog lacks
727
+ const indexDir = findInstalledPackage(root, CATALOG_INDEX_PACKAGE);
728
+ const indexDefaults = (() => { try { return indexDir === undefined ? {} : catalogDefaults(indexDir); } catch { return {}; } })();
699
729
  // Pack defaults (defaults/handlers.json, defaults/seed.ts): copied VISIBLY into the world
700
730
  // dir — the author's to edit or delete (copy = ownership; never baked into the pack).
701
- const defaultsCopies: Array<{ vendor: string; kind: 'handlers' | 'seed'; from: string; to: string }> = [];
731
+ const defaultsCopies: InitPlan['defaultsCopies'] = [];
702
732
  const signals = options.vendors ? null : detectRepoVendors(repo);
703
733
  const detected = options.vendors
704
734
  ? new Map<string, string[]>(options.vendors.map((v) => [v, ['--twins']]))
@@ -723,6 +753,14 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
723
753
  }
724
754
  for (const vendor of [...detected.keys()].sort()) {
725
755
  const detectedVia = [...(detected.get(vendor) ?? [])].sort();
756
+ const backing = catalog.has(vendor) ? undefined : VENDOR_SERVER_BACKINGS[vendor];
757
+ if (backing !== undefined) {
758
+ const service = existing?.services.find((entry) => entry.id === vendor) ?? backing.service;
759
+ if (!existingIds.has(vendor)) for (const name of endpointNames(service)) claimedEnv.set(name, vendor);
760
+ services.push(service);
761
+ vendors.push({ vendor, pack: null, detectedVia, wiring: 'backing', service, note: backing.service['//'] ?? '' });
762
+ continue;
763
+ }
726
764
  if (!catalog.has(vendor)) {
727
765
  vendors.push({
728
766
  vendor,
@@ -730,7 +768,9 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
730
768
  detectedVia,
731
769
  wiring: 'no-pack',
732
770
  service: null,
733
- note: `no packages/twin/${vendor} pack in this catalog — the proof reports it MISSING until one exists.`,
771
+ note: indexDefaults[vendor] !== undefined
772
+ ? `no ${vendor} twin installed — the catalog index's default is ${indexDefaults[vendor]!.name}@${indexDefaults[vendor]!.version} (from ${indexDefaults[vendor]!.source}): install it (\`bun add -d ${indexDefaults[vendor]!.name}@${indexDefaults[vendor]!.version}\`) and run init again. Until then the proof reports it MISSING.`
773
+ : `no ${vendor} pack in this catalog — the proof reports it MISSING until one exists.`,
734
774
  });
735
775
  continue;
736
776
  }
@@ -753,7 +793,14 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
753
793
  if (handlersTo !== null) defaultsCopies.push({ vendor, kind: 'handlers', from: defaultsHandlersPath, to: handlersTo });
754
794
  const handlersArg = handlersTo === null ? null : carried(handlersTo);
755
795
  const defaultsSeedPath = join(packDir, 'defaults', 'seed.ts');
796
+ // a protocol-3 pack's default data (src/semantics/seed.ts: calls to the vendor's own operations, as data) is copied
797
+ // beside a runner that makes each call against the twin, both the author's from then on
798
+ const semanticsSeedPath = join(packDir, 'src', 'semantics', 'seed.ts');
756
799
  if (existsSync(defaultsSeedPath)) defaultsCopies.push({ vendor, kind: 'seed', from: defaultsSeedPath, to: resolve(out, 'seeds', 'defaults', `${vendor}.ts`) });
800
+ else if (existsSync(semanticsSeedPath)) {
801
+ defaultsCopies.push({ vendor, kind: 'seed-data', from: semanticsSeedPath, to: resolve(out, 'seeds', 'defaults', `${vendor}.data.ts`) });
802
+ defaultsCopies.push({ vendor, kind: 'seed', content: seedCallsRunner(vendor), to: resolve(out, 'seeds', 'defaults', `${vendor}.ts`) });
803
+ }
757
804
  // COLOCATION (runtime contract R2a): a pack whose serve factory is known (derived into the
758
805
  // pack-facts artifact) is emitted with `colocate`, so `up` boots it inside the ONE
759
806
  // volter-world-host process instead of spawning a process per twin. The spawn `command`
@@ -766,7 +813,7 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
766
813
  const serveExport = packFacts()[vendor]?.serveExport;
767
814
  // In an app repo the twin is a PACKAGE, resolved from the world root at boot (hoisting-proof);
768
815
  // inside this checkout it is the checkout-relative path, as the cookbook and the gates expect.
769
- const boot: Pick<WorldServiceConfig, 'package' | 'command' | 'args' | 'colocate'> = catalogSource.kind === 'installed'
816
+ const boot: Pick<WorldServiceConfig, 'package' | 'command' | 'args' | 'colocate'> = catalogSource.kindOf(vendor) === 'installed'
770
817
  ? {
771
818
  package: twinPackageName(vendor),
772
819
  ...(handlersArg !== null ? { args: ['--scenario', handlersArg] } : {}),
@@ -774,8 +821,8 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
774
821
  }
775
822
  : {
776
823
  command: 'bun',
777
- args: [`packages/twin/${vendor}/src/cli.ts`, 'serve', ...(handlersArg !== null ? ['--scenario', handlersArg] : [])],
778
- ...(serveExport !== undefined ? { colocate: { module: `./packages/twin/${vendor}/src/index.ts`, export: serveExport, ...(handlersArg !== null ? { scenarioPath: handlersArg } : {}) } } : {}),
824
+ args: [relative(root, packCli(packDir)!), 'serve', ...(handlersArg !== null ? ['--scenario', handlersArg] : [])],
825
+ ...(serveExport !== undefined ? { colocate: { module: `./${relative(root, packEntry(packDir))}`, export: serveExport, ...(handlersArg !== null ? { scenarioPath: handlersArg } : {}) } } : {}),
779
826
  };
780
827
  const service: WorldServiceConfig = {
781
828
  '//': note,
@@ -789,6 +836,8 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
789
836
  ...(conflict === undefined && wiring.injectEnvTemplates ? { injectEnvTemplates: wiring.injectEnvTemplates } : {}),
790
837
  ...(conflict === undefined && wiring.cliRedirect ? { cliRedirect: wiring.cliRedirect } : {}),
791
838
  ...(wiring.serviceEnv ? { env: wiring.serviceEnv } : {}),
839
+ // a twin that holds its keys issues the applications' credential at boot (its descriptor's credentialDoor)
840
+ ...(packFacts()[vendor]?.credentialDoor !== undefined ? { issues: packFacts()[vendor]!.credentialDoor! } : {}),
792
841
  };
793
842
  if (existing !== undefined && !existingIds.has(vendor)) {
794
843
  // Check the complete proposed bindings, including any that the fresh-init conflict path
@@ -811,6 +860,7 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
811
860
  wiring: savedService !== undefined ? 'explicit-config' : conflict === undefined ? wiring.wiring : 'explicit-config',
812
861
  service: savedService ?? service,
813
862
  note: savedService !== undefined ? `Retained saved service "${vendor}" and its configured bindings.` : note,
863
+ source: { kind: catalogSource.kindOf(vendor), ...(catalogSource.version(vendor) !== undefined ? { version: catalogSource.version(vendor) } : {}) },
814
864
  });
815
865
  }
816
866
 
@@ -847,15 +897,51 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
847
897
  ...(existing?.services ?? []),
848
898
  ...services.filter((service) => !existingIds.has(service.id)),
849
899
  ].flatMap(endpointNames));
900
+ // Every name a twin of this World issues (its pack's credentialDoor fill), by the twin: the pack names them, so a name
901
+ // is issued whatever its stem (PlanetScale's DATABASE_URL, Vercel's TEAM_ID_VERCEL, Upstash's KV_REST_API_TOKEN) once
902
+ // that vendor's twin is in the World, and never when it is not.
903
+ const issuedBy = new Map<string, string>();
904
+ for (const service of [...(existing?.services ?? []).map(withIssues), ...services.filter((service) => !existingIds.has(service.id))]) {
905
+ for (const name of Object.keys(service.issues?.fill ?? {})) if (!issuedBy.has(name)) issuedBy.set(name, service.id);
906
+ }
907
+ const standInStems = new Set<string>();
850
908
  for (const entry of repoEnvEntries(repo)) {
851
909
  if (!envSources.includes(entry.source)) envSources.push(entry.source);
852
- const { name, value, source } = entry;
910
+ const { name, value, source, conflict } = entry;
853
911
  // World-owned names: the world INJECTS these at boot from the services above. Carrying a repo's
854
912
  // stale copy would shadow the live one.
855
913
  if (/_TWIN_URL$/.test(name) || /^VOLTER_/.test(name) || worldInjected.has(name)) {
856
914
  envRows.push({ name, disposition: 'unknown', source, reason: 'world-owned (injected at boot) — dropped from the emitted env' });
857
915
  continue;
858
916
  }
917
+ // The application's own origin (its base URL, its auth library's URL): only the booter knows where the app is
918
+ // reached (a local port, a browser tab's preview origin), so the World names it `$app-url` rather than copying the
919
+ // example's production address or faking one the app would then call itself at.
920
+ if (APP_ORIGIN_ENV.test(name)) {
921
+ env[name] = APP_URL_ENV;
922
+ envRows.push({ name, disposition: 'app-origin', source, reason: 'the application\'s own origin — `$app-url`: the address its booter records (`volter-world app-url <world> --set <url>`), resolved when the app attaches' });
923
+ continue;
924
+ }
925
+ // a name a twin of this World issues (its descriptor's credentialDoor): `$issue:<service>`, asked of the running twin
926
+ // at boot, so the application holds a key the twin made rather than a fixture it refuses
927
+ const issuer = issuedBy.get(name);
928
+ if (issuer !== undefined) {
929
+ env[name] = `${ISSUE_ENV_PREFIX}${issuer}`;
930
+ envRows.push({ name, disposition: 'issued', source, reason: `${issuer} credential — \`$issue:${issuer}\`: up asks the running twin to issue it` });
931
+ continue;
932
+ }
933
+ // a name some pack's credential door fills with a file (`file:<field>`) holds a path: with its issuer unselected, the
934
+ // example's path is never copied
935
+ if (fileCredentialNames().has(name)) {
936
+ env[name] = `/__volter_fake_credentials__/${name.toLowerCase().replace(/_/g, '-')}.json`;
937
+ envRows.push({ name, disposition: 'unknown', source, reason: 'credential file path — select its detected issuer to receive a World key file; the example path is never copied' });
938
+ continue;
939
+ }
940
+ // Several directories give it different values and none is the application's: the operator decides.
941
+ if (conflict !== undefined && !isCredentialShapedEnvName(name)) {
942
+ envRows.push({ name, disposition: 'unknown', source, reason: `defined differently in several places (${conflict.join('; ')}) and none of them is the application's directory — you decide`, unset: true });
943
+ continue;
944
+ }
859
945
  // STRUCTURED credential shapes first: `GOOGLE_VERTEX_JSON` / `GOOGLE_API_CREDENTIALS` carry a
860
946
  // whole JSON document the client SDK PARSES (and signs with) before any request leaves the
861
947
  // process, and neither name ends in a credential SUFFIX — so the suffix heuristics below are
@@ -878,6 +964,12 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
878
964
  });
879
965
  continue;
880
966
  }
967
+ const served = vendors.filter((v) => v.wiring === 'backing').map((v) => VENDOR_SERVER_BACKINGS[v.vendor]?.env[name]).find((value) => value !== undefined);
968
+ if (served !== undefined) {
969
+ env[name] = served;
970
+ envRows.push({ name, disposition: 'faked', source, reason: 'the key the World\'s own vendor server is started with' });
971
+ continue;
972
+ }
881
973
  const vendor = envNameVendor(name);
882
974
  if (vendor !== null) {
883
975
  env[name] = fakeEnvValue(name);
@@ -892,6 +984,14 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
892
984
  infraEntries.push({ name, source, kind: infraKind });
893
985
  continue;
894
986
  }
987
+ // A detected vendor's endpoint at a loopback server is the repository's local stand-in for the vendor (a garage for
988
+ // S3): the World routes the vendor's own host to its twin, so the name is left unset and the SDK addresses that host.
989
+ const standIn = standInStem(name, value, detected);
990
+ if (standIn !== null) {
991
+ standInStems.add(standIn);
992
+ envRows.push({ name, disposition: 'stand-in', source, reason: `a local stand-in for ${standIn}'s own host — left unset; the World routes that host to its twin`, unset: true });
993
+ continue;
994
+ }
895
995
  // RULE 3: credential-shaped means faked, even when no vendor claims the stem. That covers
896
996
  // app-local secrets (JWT_SECRET, ENCRYPTION_KEY) and untwinned vendors alike — and it is the
897
997
  // reason a live key committed to `.env.example` can never reach the emitted world.
@@ -921,34 +1021,29 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
921
1021
  envRows.push({ name, disposition: 'unknown', source, reason: 'empty in the example and not credential-shaped — you decide', unset: true });
922
1022
  }
923
1023
  }
1024
+ // A twinned vendor's credential the app's code reads that no example env declares (a script, an app whose keys come
1025
+ // from a secrets manager): the SDK refuses to start without it, so it is faked as a declared one would be. Only a
1026
+ // credential: a base-URL override the code reads (`OPENAI_BASE_URL`) is optional there, and a fake one would send
1027
+ // the SDK nowhere instead of through the injector; and never a framework's public name (`NEXT_PUBLIC_…`), whose
1028
+ // value the app's own env files hold for the browser, which a set variable would override
1029
+ for (const [name, readAt] of [...envReads.entries()].sort(([a], [b]) => a.localeCompare(b))) {
1030
+ if (name in env || envRows.some((row) => row.name === name) || /_TWIN_URL$/.test(name) || /^VOLTER_/.test(name) || worldInjected.has(name)) continue;
1031
+ if (/(?:^|_)(?:URL|URI|ENDPOINT|HOST|BASE|ORIGIN|DOMAIN|REGION|PORT)$/.test(name) || /^(?:NEXT_PUBLIC_|VITE_|REACT_APP_|EXPO_PUBLIC_|NUXT_PUBLIC_|PUBLIC_)/.test(name)) continue;
1032
+ const vendor = envNameVendor(name);
1033
+ if (vendor === null || !vendors.some((entry) => entry.vendor === vendor && entry.service !== null)) continue;
1034
+ env[name] = fakeEnvValue(name);
1035
+ envRows.push({ name, disposition: 'faked', source: readAt[0] ?? 'source', reason: `${vendor} credential read by the app's code; no example env declares it` });
1036
+ }
924
1037
 
925
- // --- native frontends ------------------------------------------------------------------------
926
- // A detected native connection (Prisma over mysql://) that a SELECTED twin serves as a native
927
- // frontend of its own state (the descriptor's `nativeTransport`) is that twin, not managed
928
- // infrastructure: a separate database would split the state the app reads through the vendor's
929
- // HTTP client from the state it writes natively — the two-client bug this exists to prevent.
930
- for (const [kind, signals] of [...infraSignals.entries()]) {
931
- const owner = vendors.find((entry) => entry.service !== null && packFacts()[entry.vendor]?.nativeTransport?.protocol === kind);
932
- if (owner === undefined || owner.service === null) continue;
933
- const native = packFacts()[owner.vendor]!.nativeTransport!;
934
- const id = `${owner.vendor}-${kind}`;
935
- const frontend: WorldServiceConfig = {
936
- '//': `${owner.vendor}'s native ${kind} frontend: the same state as the "${owner.vendor}" twin over the ${kind} wire protocol (declare the HTTP twin first; it reads ${native.upstreamEnv} from it).`,
937
- id,
938
- type: 'twin',
939
- ...(catalogSource.kind === 'installed'
940
- ? { package: twinPackageName(owner.vendor), args: [native.flag] }
941
- : { command: 'bun', args: [`packages/twin/${owner.vendor}/src/cli.ts`, 'serve', native.flag] }),
942
- ...(catalogSource.version(owner.vendor) !== undefined ? { version: catalogSource.version(owner.vendor) } : {}),
943
- port: 'auto',
944
- injectEnvTemplates: Object.fromEntries([...signals].sort().map((name) => [name, `${kind}://twin:twin@${'${host}'}:${'${port}'}/twin`])),
945
- };
946
- services.splice(services.indexOf(owner.service) + 1, 0, frontend);
947
- for (const entry of infraEntries.filter((candidate) => candidate.kind === kind)) {
948
- envRows.push({ name: entry.name, disposition: 'unknown', source: entry.source, reason: `${kind} connection string — injected at boot by the "${id}" twin service (${owner.vendor}'s native frontend over the same state)` });
949
- }
950
- infraSignals.delete(kind);
951
- infraEntries.splice(0, infraEntries.length, ...infraEntries.filter((candidate) => candidate.kind !== kind));
1038
+ // the stand-in's own region (garage's `garage`), where it is not a region of the vendor's shape (`eu-west-1`): the SDK
1039
+ // would sign for a region the vendor does not have
1040
+ for (const row of envRows) {
1041
+ const stem = [...standInStems].find((candidate) => row.name === `${candidate}_REGION`);
1042
+ if (stem === undefined || row.disposition !== 'kept' || /^[a-z]{2}(-[a-z]+)+-\d+$/.test(env[row.name] ?? '')) continue;
1043
+ delete env[row.name];
1044
+ row.disposition = 'stand-in';
1045
+ row.reason = `the local stand-in's region, not one ${stem}'s vendor has — left unset`;
1046
+ row.unset = true;
952
1047
  }
953
1048
 
954
1049
  // --- infra ----------------------------------------------------------------------------------
@@ -1017,14 +1112,14 @@ export function planWorldInit(name: string, repoPath: string, options: InitOptio
1017
1112
  external: {
1018
1113
  up: ['volter-world-infra', 'up'],
1019
1114
  status: ['volter-world-infra', 'status'],
1020
- discover: compose.flatMap((service) => service.signals.map((name) => ({ as: name, jsonPath: `connections.${name}` }))),
1115
+ discover: compose.flatMap((service) => service.signals.map((name) => ({ as: name, jsonPath: `connections.${name}`, ...(['postgres', 'mysql', 'mongodb', 'redis'].includes(service.kind) ? { protocol: service.kind } : {}) }))),
1021
1116
  down: ['volter-world-infra', 'down'],
1022
1117
  readyWhen: { command: 'volter-world-infra', args: ['status'], timeoutMs: 120000, intervalMs: 500 },
1023
1118
  },
1024
1119
  });
1025
1120
  }
1026
1121
 
1027
- const mergedServices = existing === undefined ? services : [...existing.services, ...services.filter((service) => !existingIds.has(service.id))];
1122
+ const mergedServices = existing === undefined ? services : [...existing.services.map(withIssues), ...services.filter((service) => !existingIds.has(service.id))];
1028
1123
  if (existing !== undefined) {
1029
1124
  const owners = new Map<string, string>();
1030
1125
  for (const service of mergedServices) {
@@ -1198,7 +1293,7 @@ export function writeWorldInit(plan: InitPlan, options: { force?: boolean } = {}
1198
1293
  // are the author's files now (edit or delete; regenerate a fresh world for fresh defaults).
1199
1294
  for (const copy of plan.defaultsCopies) {
1200
1295
  mkdirSync(dirname(copy.to), { recursive: true });
1201
- if (!(options.force === true && existsSync(copy.to))) writeFileSync(copy.to, readFileSync(copy.from));
1296
+ if (!(options.force === true && existsSync(copy.to))) writeFileSync(copy.to, copy.content ?? readFileSync(copy.from!));
1202
1297
  }
1203
1298
  if (plan.seedEntry !== null) {
1204
1299
  mkdirSync(dirname(plan.seedEntry.storyPath), { recursive: true });
@@ -1222,6 +1317,31 @@ export function writeWorldInit(plan: InitPlan, options: { force?: boolean } = {}
1222
1317
  }
1223
1318
  }
1224
1319
 
1320
+ /** The runner of a protocol-3 pack's default data (its `src/semantics/seed.ts`, copied beside it as `<vendor>.data.ts`):
1321
+ * each call, in order, against the twin at its `<VENDOR>_TWIN_URL`, a refusal stopping the seed with the twin's answer. */
1322
+ export function seedCallsRunner(vendor: string): string {
1323
+ const stem = vendor.toUpperCase().replace(/[^A-Z0-9]/g, '_');
1324
+ return `// ${vendor}'s default data, as its twin's pack declares it (./${vendor}.data.ts: calls to the vendor's own
1325
+ // operations), made against the twin in order. Ownership transferred on copy: both files are yours — edit or delete.
1326
+ import { seed as calls } from './${vendor}.data.ts';
1327
+
1328
+ export async function seed(): Promise<void> {
1329
+ // a twin with a root is the vendor's real account: its default data is not written (VOLTER_SEED_SKIP)
1330
+ if ((process.env.VOLTER_SEED_SKIP ?? '').split(',').includes('${vendor}')) return;
1331
+ const base = process.env.${stem}_TWIN_URL;
1332
+ if (!base) throw new Error('${stem}_TWIN_URL is not set — bring the world up and run the seed with its env (volter-world attach/run)');
1333
+ for (const call of calls) {
1334
+ const res = await fetch(\`\${base.replace(/\\/$/, '')}\${call.path}\`, {
1335
+ method: call.method,
1336
+ headers: { ...(call.body !== undefined ? { 'content-type': 'application/json' } : {}), ...(call.headers ?? {}) },
1337
+ ...(call.body !== undefined ? { body: JSON.stringify(call.body) } : {}),
1338
+ });
1339
+ if (!res.ok) throw new Error(\`${vendor} seed: \${call.method} \${call.path}: \${res.status} \${await res.text()}\`);
1340
+ }
1341
+ }
1342
+ `;
1343
+ }
1344
+
1225
1345
  /**
1226
1346
  * Plan, emit, and PROVE. The coverage proof runs against the just-written config (by path, so it
1227
1347
  * reads exactly the bytes on disk rather than the in-memory plan) — `ok` is the proof's verdict and
@@ -1287,9 +1407,10 @@ export function formatInitReport(result: InitResult): string {
1287
1407
  if (plan.vendors.length === 0) lines.push(' (no external vendor dependencies detected in the repo)');
1288
1408
  else {
1289
1409
  lines.push(...table(
1290
- ['vendor', 'wiring', 'inject-env', 'detected-via'],
1410
+ ['vendor', 'source', 'wiring', 'inject-env', 'detected-via'],
1291
1411
  plan.vendors.map((vendor) => [
1292
1412
  vendor.vendor,
1413
+ vendor.source ? `${vendor.source.kind}${vendor.source.version ? ` ${vendor.source.version}` : ''}` : '-',
1293
1414
  vendor.wiring,
1294
1415
  [vendor.service?.injectEnv, ...Object.keys(vendor.service?.injectEnvTemplates ?? {})].filter(Boolean).join(', ') || '-',
1295
1416
  vendor.detectedVia.join('; '),
@@ -1303,7 +1424,7 @@ export function formatInitReport(result: InitResult): string {
1303
1424
  lines.push('');
1304
1425
  const counts = (disposition: EnvDisposition): InitEnvRow[] => plan.envRows.filter((row) => row.disposition === disposition);
1305
1426
  lines.push(`Env (${plan.envRows.length} name(s) from ${plan.envSources.join(', ') || 'no example env file'}):`);
1306
- for (const disposition of ['faked', 'kept', 'compose', 'placeholder', 'unknown'] as EnvDisposition[]) {
1427
+ for (const disposition of ['faked', 'kept', 'app-origin', 'stand-in', 'compose', 'placeholder', 'unknown'] as EnvDisposition[]) {
1307
1428
  const rows = counts(disposition);
1308
1429
  if (rows.length === 0) continue;
1309
1430
  lines.push(` ${disposition.padEnd(11)} ${String(rows.length).padStart(3)} ${rows.map((row) => row.name).join(', ')}`);
@@ -1,10 +1,10 @@
1
1
  // Durable ownership of one World. This inventory does not allocate or reserve capacity.
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
- import { volterHome } from '@volter/world-core';
4
+ import { volterHome } from '@volter/world-core/runtime';
5
5
  import { hostname } from 'node:os';
6
6
  import { dirname, join, resolve } from 'node:path';
7
- import { stateDirName, withFileLock } from '@volter/world-core';
7
+ import { stateDirName, withFileLock } from '@volter/world-core/runtime';
8
8
  import { checkedOwner } from './consumers.ts';
9
9
 
10
10
  export type WorldLifecycleRecord = {
@@ -5,7 +5,7 @@
5
5
  // and `volter world view` both make them with this; a hosted World's supervisor makes its own.
6
6
  import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
7
7
  import { dirname, join } from 'node:path';
8
- import { stateDirName, withStateRemoval } from '@volter/world-core';
8
+ import { stateDirName, withStateRemoval } from '@volter/world-core/runtime';
9
9
  import { clockFile } from './runtime.ts';
10
10
  import { loadWorldConfig } from './configs.ts';
11
11
  import { TOKEN_HEADER, type BranchDoors, type BranchRow, type MountedWorld } from './served-world.ts';