@volter/world-runtime 3.0.143 → 3.0.145

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.
@@ -7,7 +7,7 @@ import { createRequire } from 'node:module';
7
7
  import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
8
8
  import { tmpdir } from 'node:os';
9
9
  import { join, resolve } from 'node:path';
10
- import { activateScript, branchWorld, checkoutWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, resetWorld, seedWorld, worldOrigin, approveWorldChangeset, writeWorldClock, checkPrerequisites, coverWorld, createWorldChangeset, diffWorld, doctorWorld, downWorld, findWorldChangeset, formatCoverageReport, formatInitReport, formatPrerequisiteChecks, formatProjectInspection, initWorld, inspectProject, isRemoteWorldRef, listWorldChangesets, listWorlds, markWorld, migrateWorldConfig, readReflectRoutes, reflectRoutesPath, replayWorldChangeset, resolveWorldRef, attachWorld, retireWorldConsumers, runWorld, shareWorldServices, shellWorld, startReflectFront, startReflectResolver, statusWorld, statusWorldChangeset, tailWorldActions, unshareWorld, urlsWorld, urlWorld, verifyWorldChangeset, worldManifest, writeReflectRoutes, readReflectManifest, writeReflectManifest, clearReflectManifest, composeOverrideForReflect, splitDockerComposeArgs, dockerComposeWithOverride, ATTACHED_CA_PATH } from "./index.js";
10
+ import { activateScript, branchWorld, checkoutWorld, fetchFromOrigin, pushWorldChangeset, rebaseWorldChangeset, resetWorld, seedWorld, worldOrigin, approveWorldChangeset, writeWorldClock, checkPrerequisites, coverWorld, createWorldChangeset, diffWorld, doctorWorld, downWorld, findWorldChangeset, formatCoverageReport, recordCoverage, formatInitReport, formatPrerequisiteChecks, formatProjectInspection, initWorld, inspectProject, isRemoteWorldRef, listWorldChangesets, listWorlds, markWorld, migrateWorldConfig, readReflectRoutes, reflectRoutesPath, replayWorldChangeset, resolveWorldRef, attachWorld, retireWorldConsumers, runWorld, shareWorldServices, shellWorld, startReflectFront, startReflectResolver, statusWorld, statusWorldChangeset, tailWorldActions, unshareWorld, urlsWorld, urlWorld, verifyWorldChangeset, worldManifest, writeReflectRoutes, readReflectManifest, writeReflectManifest, clearReflectManifest, composeOverrideForReflect, splitDockerComposeArgs, dockerComposeWithOverride, ATTACHED_CA_PATH } from "./index.js";
11
11
  import { formatApplication, formatChangeset, formatRebaseReport, formatChangesetStatus, formatLedgerDelta, formatReplayReport, formatVerification, parseVerifierExpression } from '@volter/world-core/runtime';
12
12
  import { advertiseWorldManifest, fetchRemoteManifest, MANIFEST_PATH, remoteAttachEnv, startManifestServer } from "./serve.js";
13
13
  import { clockFile, instanceDir, pruneWorlds, withoutVaultSignIn } from "./runtime.js";
@@ -57,7 +57,7 @@ function printHelp() {
57
57
  # emission. Exit 0 = ready to \`up\`; exit 1 = the honest worklist.
58
58
  # Deterministic: same repo + same pack catalog => identical bytes.
59
59
  # init DIAGNOSES; it never boots — it prints the exact next commands
60
- volter-world up <config> --env-out=<path> [--owner <label>] [--name <name>] [--mode local|share|sealed] [--isolation process|colocated|worker] [--keep-state] [--app-url <url>] [--json] [--root <repo>]
60
+ volter-world up <config> --env-out=<path> [--owner <label>] [--name <name>] [--mode local|share|sealed] [--isolation process|colocated|worker] [--keep-state] [--app-url <url>] [--in-process] [--json] [--root <repo>]
61
61
  volter-world outdated <world> [--json] — each mounted twin: pinned / mounted / catalog-current version and protocol standing (R17)
62
62
  volter-world audit <world> [--json] — each mounted twin's grade, as the catalog records it (R17)
63
63
  volter-world down <name> [--grace-ms <ms>] [--purge] [--if-instance <token>] [--root <repo>] # SIGTERM, then SIGKILL survivors after the grace (default 5000ms)
@@ -516,6 +516,17 @@ export async function main() {
516
516
  throw new Error('volter-world covers: --repo <path> is required (the application repo to prove coverage for)');
517
517
  const report = coverWorld(subject, repo, { root, allowUnknown: rest.includes('--allow-unknown'), acknowledge: acknowledgeOptions(rest, 'covers') });
518
518
  process.stdout.write(rest.includes('--json') ? `${JSON.stringify(report, null, 2)}\n` : formatCoverageReport(report));
519
+ // `covers` is a reading. It keeps the proof it printed only as the plain verb on a World's own config: with --json
520
+ // it prints and writes nothing, and it never rewrites the config a running instance booted from. Whether it kept
521
+ // the proof is said on stderr, beside the report and outside its JSON.
522
+ if (!rest.includes('--json')) {
523
+ if (report.worldSource !== 'config')
524
+ process.stderr.write(`coverage not recorded: ${report.world} was read from its instance (${report.worldPath}); the config it booted from is not rewritten\n`);
525
+ else if (recordCoverage(report, root))
526
+ process.stderr.write(`coverage recorded in ${report.configPath}\n`);
527
+ else
528
+ process.stderr.write(`coverage not recorded: ${report.configPath} is a format-1 document, which has no discovery section, or is no longer on disk\n`);
529
+ }
519
530
  if (!report.ok)
520
531
  process.exit(1);
521
532
  return;
@@ -1,4 +1,4 @@
1
- import { type WorldSelection, type WorldServiceConfig, type WorldServiceInstance, type WorldUsage } from './schema.js';
1
+ import { type WorldCoverage, type WorldSelection, type WorldServiceConfig, type WorldServiceInstance, type WorldUsage } from './schema.js';
2
2
  export type CoverageStatus = 'covered' | 'missing' | 'uninterceptable' | 'unknown-sdk' | 'acknowledged' | 'excluded';
3
3
  export type CoverageRow = {
4
4
  /** vendor key for mapped vendors; the raw dep/env name for unknown-sdk rows */
@@ -16,6 +16,9 @@ export type CoverageReport = {
16
16
  /** 'instance' when a booted/recorded instance was read; 'config' for the stable config */
17
17
  worldSource: 'instance' | 'config';
18
18
  worldPath: string;
19
+ /** The World's config, where the proof is kept (`recordCoverage`): `worldPath` itself for a config, the config
20
+ * an instance booted from otherwise. */
21
+ configPath: string;
19
22
  repo: string;
20
23
  twinsInWorld: string[];
21
24
  rows: CoverageRow[];
@@ -121,6 +124,8 @@ type WorldTwinInventory = {
121
124
  services: Array<WorldServiceConfig | WorldServiceInstance>;
122
125
  source: 'instance' | 'config';
123
126
  path: string;
127
+ /** The config the World is declared in: the one read, or the one its instance booted from. */
128
+ configPath: string;
124
129
  /** normalized identifier → id record (e.g. 'stripe' → { label: 'stripe (service)', … }) */
125
130
  ids: Map<string, WorldTwinId>;
126
131
  connections: Map<string, {
@@ -180,5 +185,20 @@ export type RepoVendorSignals = {
180
185
  * and source lists, so callers that preserve it are deterministic. */
181
186
  export declare function detectRepoVendors(repoPath: string, services?: Array<WorldServiceConfig | WorldServiceInstance>): RepoVendorSignals;
182
187
  export declare function coverWorld(world: string, repoPath: string, options?: CoverageOptions): CoverageReport;
188
+ /**
189
+ * The report as the data `init` keeps in the config (`discovery.coverage`): the same rows `formatCoverageReport`
190
+ * prints, grouped by the status each verdict line counts. Every push to `covered`, `missing`, `uninterceptable`,
191
+ * `unknown`, `acknowledged` and `excluded` in `coverWorld` adds the row of that status, so the lists here name what
192
+ * those lines name. Paths (`worldPath`, `configPath`, `repo`) stay out: the config is portable and init's bytes are deterministic.
193
+ */
194
+ export declare function coverageRecord(report: CoverageReport): WorldCoverage;
195
+ /**
196
+ * Keep a proof just made in the World's config, replacing the record of an earlier one. It answers true when it
197
+ * wrote the record and false when it wrote nothing: a format-1 document has no `discovery` section and a World whose
198
+ * config is no longer on disk has nowhere to keep it. The caller decides whether to record at all and says the
199
+ * answer (`init` writes its own record into the config it makes): `covers` records only as the plain verb on a
200
+ * World's own config, never with --json and never into the config a running instance booted from (cli-commands.ts).
201
+ */
202
+ export declare function recordCoverage(report: CoverageReport, root?: string): boolean;
183
203
  export declare function formatCoverageReport(report: CoverageReport): string;
184
204
  export {};
@@ -25,8 +25,9 @@ import { errorMessage } from "./error-message.js";
25
25
  import { existsSync, readFileSync } from 'node:fs';
26
26
  import { basename, dirname, join, relative, resolve } from 'node:path';
27
27
  import { fileURLToPath } from 'node:url';
28
- import { installedPackageDir, packVendor, vendorOfPackModule } from '@volter/world-core/runtime';
29
- import { loadWorldConfig } from "./configs.js";
28
+ import { getActiveWorldStore, installedPackageDir, packVendor, vendorOfPackModule } from '@volter/world-core/runtime';
29
+ import { loadWorldConfig, writeWorldConfig } from "./configs.js";
30
+ import { runtimeManifest } from "./package-manifest.js";
30
31
  import { VENDOR_SERVER_BACKINGS } from "./backings.js";
31
32
  import { injectableVendorKeys, loadInject, twinUrlVendorFor } from "./inject-map.js";
32
33
  import { connectionProtocol, projectConnections, projectDependencies, projectManifestDirs, projectEnvNames, projectFetchUrls, projectNpmRegistries, PYPI_TWINS, SDK_TWINS } from "./project-inspect.js";
@@ -472,7 +473,7 @@ export function worldTwinInventory(world, root) {
472
473
  if (stem)
473
474
  addId(ids, normalizeId(stem[1]), `env ${key}`, twinUrlVendorFor(key) !== null);
474
475
  }
475
- return { services: Object.values(status.services), source: 'instance', path: `${status.dirs.instance}/instance.json`, ids, connections: serviceConnections(Object.values(status.services), status.env, root), selection: status.selection, declared: declaredTwin(Object.values(status.services), status.env, root) };
476
+ return { services: Object.values(status.services), source: 'instance', path: `${status.dirs.instance}/instance.json`, configPath: status.configPath, ids, connections: serviceConnections(Object.values(status.services), status.env, root), selection: status.selection, declared: declaredTwin(Object.values(status.services), status.env, root) };
476
477
  }
477
478
  catch (error) {
478
479
  instanceError = errorMessage(error);
@@ -491,7 +492,7 @@ export function worldTwinInventory(world, root) {
491
492
  if (value.startsWith(ISSUE_ENV_PREFIX))
492
493
  connections.set(name, { protocol: 'issued', label: `${value.slice(ISSUE_ENV_PREFIX.length)} (${name}; issued)` });
493
494
  }
494
- return { services: config.services, source: 'config', path, ids, connections, selection: config.selection, declared: declaredTwin(config.services, undefined, root) };
495
+ return { services: config.services, source: 'config', path, configPath: path, ids, connections, selection: config.selection, declared: declaredTwin(config.services, undefined, root) };
495
496
  }
496
497
  catch (error) {
497
498
  const configError = errorMessage(error);
@@ -895,6 +896,7 @@ export function coverWorld(world, repoPath, options = {}) {
895
896
  world,
896
897
  worldSource: inventory.source,
897
898
  worldPath: inventory.path,
899
+ configPath: inventory.configPath,
898
900
  repo,
899
901
  twinsInWorld: [...inventory.ids.values()].map((id) => id.label).sort(),
900
902
  rows,
@@ -908,6 +910,46 @@ export function coverWorld(world, repoPath, options = {}) {
908
910
  ok: missing.length === 0 && uninterceptable.length === 0 && (allowUnknown || unknown.length === 0),
909
911
  };
910
912
  }
913
+ /**
914
+ * The report as the data `init` keeps in the config (`discovery.coverage`): the same rows `formatCoverageReport`
915
+ * prints, grouped by the status each verdict line counts. Every push to `covered`, `missing`, `uninterceptable`,
916
+ * `unknown`, `acknowledged` and `excluded` in `coverWorld` adds the row of that status, so the lists here name what
917
+ * those lines name. Paths (`worldPath`, `configPath`, `repo`) stay out: the config is portable and init's bytes are deterministic.
918
+ */
919
+ export function coverageRecord(report) {
920
+ const entry = (row) => ({
921
+ evidence: row.detectedVia,
922
+ ...(row.connection === undefined ? {} : { connection: row.connection }),
923
+ ...(row.twinInWorld === null ? {} : { twinInWorld: row.twinInWorld }),
924
+ });
925
+ const of = (status) => report.rows.filter((row) => row.status === status).map((row) => ({ vendor: row.vendor, ...entry(row) }));
926
+ return {
927
+ producer: { package: '@volter/world-runtime', version: runtimeManifest().version ?? 'unversioned' },
928
+ ok: report.ok,
929
+ covered: of('covered'),
930
+ notCovered: of('missing'),
931
+ uninterceptable: of('uninterceptable'),
932
+ unknownSdk: report.rows.filter((row) => row.status === 'unknown-sdk').map((row) => ({ signal: row.vendor, ...entry(row) })),
933
+ acknowledged: of('acknowledged').map((row) => ({ ...row, reason: report.acknowledged.find((held) => held.vendor === row.vendor).reason })),
934
+ excludedBySelection: of('excluded'),
935
+ };
936
+ }
937
+ /**
938
+ * Keep a proof just made in the World's config, replacing the record of an earlier one. It answers true when it
939
+ * wrote the record and false when it wrote nothing: a format-1 document has no `discovery` section and a World whose
940
+ * config is no longer on disk has nowhere to keep it. The caller decides whether to record at all and says the
941
+ * answer (`init` writes its own record into the config it makes): `covers` records only as the plain verb on a
942
+ * World's own config, never with --json and never into the config a running instance booted from (cli-commands.ts).
943
+ */
944
+ export function recordCoverage(report, root = process.cwd()) {
945
+ if (!getActiveWorldStore().exists(report.configPath))
946
+ return false;
947
+ const { path, config } = loadWorldConfig(report.configPath, root);
948
+ if (config.manifestVersion !== 2)
949
+ return false;
950
+ writeWorldConfig(path, { ...config, coverage: coverageRecord(report) });
951
+ return true;
952
+ }
911
953
  export function formatCoverageReport(report) {
912
954
  const lines = [];
913
955
  lines.push(`World ${report.world} (${report.worldSource}: ${report.worldPath})`);
@@ -1,4 +1,4 @@
1
- export type { WorldConfig, WorldInstance, WorldIsolation, WorldMode, WorldServiceConfig, WorldServiceType, WorldExternalServiceConfig, WorldExternalDiscover, WorldExternalDiscoverSource, WorldExternalReadyWhen, WorldRunOutcome, WorldRunRecord, WorldResourceRequirements, WorldManifestV2, WorldSelection, WorldSelectionSelector, WorldUsage, } from './schema.js';
1
+ export type { WorldConfig, WorldInstance, WorldIsolation, WorldMode, WorldServiceConfig, WorldServiceType, WorldExternalServiceConfig, WorldExternalDiscover, WorldExternalDiscoverSource, WorldExternalReadyWhen, WorldRunOutcome, WorldRunRecord, WorldResourceRequirements, WorldManifestV2, WorldCoverage, WorldCoverageEntry, WorldSelection, WorldSelectionSelector, WorldUsage, } from './schema.js';
2
2
  export { DEFAULT_WORLD_SELECTION, LEGACY_WORLD_SELECTION, WORLD_SCHEMA_VERSION, WORLD_USAGES, worldConfigDocument, worldSelectionIncludes } from './schema.js';
3
3
  export type { WorldChangesetRules } from './schema.js';
4
4
  export { loadWorldConfig, migrateWorldConfig, resolveConfigPath, worldChangesetRules, writeWorldConfig } from './configs.js';
@@ -17,7 +17,7 @@ export { inspectProject, formatProjectInspection, projectDependencies, projectEn
17
17
  export type { ProjectInspection } from './project-inspect.js';
18
18
  export { fakeEnvValue, fakeGoogleOAuthClientJson, fakeGoogleServiceAccountJson, isGoogleOAuthClientEnvName, isGoogleServiceAccountEnvName } from './fixture-env.js';
19
19
  export type { FakeServiceAccountOptions } from './fixture-env.js';
20
- export { coverWorld, detectRepoVendors, envNameVendor, formatCoverageReport, injectorVendorKeysFor, isCredentialShapedEnvName, worldTwinInventory, } from './covers.js';
20
+ export { coverWorld, coverageRecord, detectRepoVendors, envNameVendor, formatCoverageReport, injectorVendorKeysFor, isCredentialShapedEnvName, recordCoverage, worldTwinInventory, } from './covers.js';
21
21
  export type { CoverageReport, CoverageRow, CoverageStatus, CoverageOptions, RepoVendorSignals } from './covers.js';
22
22
  export { formatInitReport, initWorld, packCatalog, planWorldInit, renderEnvFile, writeWorldInit } from './init.js';
23
23
  export { STATE_GITIGNORE } from './init.js';
package/dist/src/index.js CHANGED
@@ -14,7 +14,7 @@ export { inspectProject, formatProjectInspection, projectDependencies, projectEn
14
14
  // Structurally valid fake credentials for world configs (fixture env) — see fixture-env.ts
15
15
  // for the pattern (a fake a client SDK parses/signs with must be structurally real).
16
16
  export { fakeEnvValue, fakeGoogleOAuthClientJson, fakeGoogleServiceAccountJson, isGoogleOAuthClientEnvName, isGoogleServiceAccountEnvName } from "./fixture-env.js";
17
- export { coverWorld, detectRepoVendors, envNameVendor, formatCoverageReport, injectorVendorKeysFor, isCredentialShapedEnvName, worldTwinInventory, } from "./covers.js";
17
+ export { coverWorld, coverageRecord, detectRepoVendors, envNameVendor, formatCoverageReport, injectorVendorKeysFor, isCredentialShapedEnvName, recordCoverage, worldTwinInventory, } from "./covers.js";
18
18
  // `volter-world init` — the deterministic front door: detect the repo's vendors (covers), emit a
19
19
  // world config + env file OUTSIDE the repo (fixture-env), and prove the emission with `covers`.
20
20
  export { formatInitReport, initWorld, packCatalog, planWorldInit, renderEnvFile, writeWorldInit } from "./init.js";
package/dist/src/infra.js CHANGED
@@ -12,7 +12,7 @@ import { delimiter, dirname, join } from 'node:path';
12
12
  import { signalChild } from './process-signal.mjs';
13
13
  import { spawn } from 'node:child_process';
14
14
  import { fileURLToPath } from 'node:url';
15
- import { infraConnections, parseInfraDefinition } from "./pglite-backing.js";
15
+ import { containerlessDataDirectory, infraConnections, liveDatabaseBehind, liveDatabaseMigration, developmentMigratesAtRuntime, DEVELOPMENT_MIGRATION_NOTICE, parseInfraDefinition } from "./pglite-backing.js";
16
16
  import { errorMessage } from "./error-message.js";
17
17
  /** Whether a program is on PATH, read from the file system without starting a process: where a missing program
18
18
  * still costs a process round trip (a browser tab's kernel), the container probe is not spawned at all. */
@@ -98,6 +98,10 @@ export async function runInfrastructure(phase, env, io, signal) {
98
98
  // prepare: the image build's step that makes the World's PGlite cluster once, on its host, so the World's first up
99
99
  // opens it instead of running initdb (pglite-backing.ts, pglitePrepare; ADR 0018). It needs the World's
100
100
  // definition, not an instance.
101
+ // A backing the World's environment forces is said where the backing is said, and in the status fact: forcing one
102
+ // is a choice someone made for this World, and it is never silent.
103
+ const forcedBy = (backing) => env.VOLTER_WORLD_INFRA_BACKING === backing ? ' (forced by VOLTER_WORLD_INFRA_BACKING)' : '';
104
+ const backingFact = (backing) => ({ backing: { is: backing, by: env.VOLTER_WORLD_INFRA_BACKING === backing ? 'forced' : 'chosen' } });
101
105
  if (phase === 'prepare') {
102
106
  const config = env.VOLTER_WORLD_CONFIG;
103
107
  const declaredDefinition = config ? join(dirname(config), 'world.infrastructure.yml') : undefined;
@@ -111,12 +115,24 @@ export async function runInfrastructure(phase, env, io, signal) {
111
115
  return 0;
112
116
  }
113
117
  const { pglitePrepare } = await import("./pglite-backing.js");
118
+ const { nativePrepare, nativePostgresTools } = await import("./native-postgres-backing.js");
119
+ // The backing up would choose for this PostgreSQL: native where the PostgreSQL programs are (containerless rule
120
+ // 2; in a tab image, the tab's own build), PGlite otherwise or when forced.
121
+ const native = env.VOLTER_WORLD_INFRA_BACKING !== 'pglite' && nativePostgresTools(env) !== undefined;
122
+ // The database prepare step's own output (prisma's "N migrations found … No pending migrations") is the
123
+ // preparation's record, said where its caller reads it.
124
+ const launch = { env, ...(signal ? { signal } : {}), note: (what) => io.out(`${what.trimEnd()}\n`) };
114
125
  try {
115
- io.out(`managed infrastructure prepared ${await pglitePrepare(postgres[0], config, { env, ...(signal ? { signal } : {}) })}\n`);
126
+ const prepared = native
127
+ ? await nativePrepare(postgres[0], readFileSync(declaredDefinition, 'utf8'), config, launch)
128
+ : await pglitePrepare(postgres[0], config, launch);
129
+ io.out(`managed infrastructure prepared ${prepared} (backing: ${native ? 'native PostgreSQL' : 'PGlite'}${forcedBy(native ? 'native' : 'pglite')})\n`);
116
130
  return 0;
117
131
  }
118
132
  catch (error) {
119
133
  io.err(`managed infrastructure prepare failed: ${errorMessage(error)}\n`);
134
+ // Said where the person or agent reading the failure stands: the fix is here, in the preparation.
135
+ io.err('The fix is in this build-time preparation (the step above, or what it runs against). Migrating at run time is not the remedy for a failed preparation.\n');
120
136
  return 1;
121
137
  }
122
138
  }
@@ -131,6 +147,19 @@ export async function runInfrastructure(phase, env, io, signal) {
131
147
  io.err('managed infrastructure: declared definition is missing\n');
132
148
  return 2;
133
149
  }
150
+ // A development World says so every time it comes up: its use is never silent.
151
+ if (phase === 'up' && developmentMigratesAtRuntime(worldConfig))
152
+ io.out(`${DEVELOPMENT_MIGRATION_NOTICE}\n`);
153
+ // What the app's commands are told of the World's database (architecture, managed infrastructure): the declared
154
+ // step's command while the live data carries the migration the image expects, the same command when the World
155
+ // declares the step and the live data does not carry it, and whether this is a development World whose app migrates when it runs. `postgresData` is the serving backing's data directory, set where a status knows it.
156
+ let postgresData;
157
+ const databaseFacts = () => {
158
+ const migrated = postgresData ? liveDatabaseMigration(worldConfig, env, postgresData) : undefined;
159
+ const behind = postgresData ? liveDatabaseBehind(worldConfig, env, postgresData) : undefined;
160
+ return { ...(migrated || behind ? { database: { ...(migrated ? { migrated } : {}), ...(behind ? { behind } : {}) } } : {}),
161
+ ...(developmentMigratesAtRuntime(worldConfig) ? { development: { migrateDatabaseAtRuntime: true } } : {}) };
162
+ };
134
163
  const base = ['compose', '-f', definition];
135
164
  const run = async (args) => {
136
165
  const ran = await runCommand('docker', [...base, ...args], { env: { ...env, VOLTER_WORLD_DATA: worldData }, timeout: 120_000, ...(signal ? { signal } : {}) });
@@ -245,7 +274,7 @@ export async function runInfrastructure(phase, env, io, signal) {
245
274
  return 1;
246
275
  }
247
276
  if (phase === 'up') {
248
- io.out('managed infrastructure backing: containerless (no container runtime)\n');
277
+ io.out(`managed infrastructure backing: containerless${forcedBy('pglite') || ' (no container runtime)'}\n`);
249
278
  if (services.some((s) => s.kind === 'postgres')) {
250
279
  io.out('managed infrastructure: PostgreSQL is PGlite, one session every connection takes in turn; a read through '
251
280
  + 'the pool inside a transaction waits on itself until that transaction ends. A postgres server on PATH, or '
@@ -263,11 +292,13 @@ export async function runInfrastructure(phase, env, io, signal) {
263
292
  }
264
293
  if (phase === 'status') {
265
294
  const status = await pgliteStatus(services, worldData);
295
+ if (services.some((s) => s.kind === 'postgres'))
296
+ postgresData = join(worldData, containerlessDataDirectory('postgres'));
266
297
  if (!status.ok) {
267
298
  io.err(`managed infrastructure status failed: ${status.ready}/${services.length} declared services are ready\n`);
268
299
  return 1;
269
300
  }
270
- io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env) })}\n`);
301
+ io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...backingFact('pglite'), ...databaseFacts() })}\n`);
271
302
  return 0;
272
303
  }
273
304
  await pgliteDown(services, worldData);
@@ -336,7 +367,7 @@ export async function runInfrastructure(phase, env, io, signal) {
336
367
  if (phase === 'up')
337
368
  io.out('managed infrastructure ready\n');
338
369
  else if (phase === 'status')
339
- io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env) })}\n`);
370
+ io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...databaseFacts() })}\n`);
340
371
  else
341
372
  io.out('managed infrastructure stopped\n');
342
373
  return 0;
@@ -357,7 +388,7 @@ export async function runInfrastructure(phase, env, io, signal) {
357
388
  }
358
389
  try {
359
390
  if (phase === 'up') {
360
- io.out('managed infrastructure backing: native PostgreSQL (multi-session)\n');
391
+ io.out(`managed infrastructure backing: native PostgreSQL (multi-session)${forcedBy('native')}\n`);
361
392
  await nativePostgresUp(postgres, worldData, readFileSync(definition, 'utf8'), launch);
362
393
  try {
363
394
  await pgliteUp(others, worldData, launch);
@@ -377,7 +408,8 @@ export async function runInfrastructure(phase, env, io, signal) {
377
408
  const ready = others.length === 0 || (await pgliteStatus(others, worldData)).ok;
378
409
  if (!ready || !await nativePostgresStatus(worldData))
379
410
  return 1;
380
- io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env) })}\n`);
411
+ postgresData = join(worldData, 'native-postgres-data');
412
+ io.out(`${JSON.stringify({ ok: true, services: declared.length, connections: infraConnections(declared, env), ...backingFact('native'), ...databaseFacts() })}\n`);
381
413
  }
382
414
  else {
383
415
  await pgliteDown(others, worldData);
@@ -393,6 +425,8 @@ export async function runInfrastructure(phase, env, io, signal) {
393
425
  }
394
426
  const only = composed ?? [];
395
427
  if (phase === 'up') {
428
+ if (forcedBy('docker'))
429
+ io.out(`managed infrastructure backing: container runtime${forcedBy('docker')}\n`);
396
430
  const result = await run(['up', '-d', '--wait', ...only]);
397
431
  if (result.status !== 0)
398
432
  return fail(result);
@@ -411,7 +445,7 @@ export async function runInfrastructure(phase, env, io, signal) {
411
445
  io.err(`managed infrastructure status failed: ${runningNames.length + twinned.length + native.length}/${expectedNames.length + twinned.length + native.length} declared services are ready\n`);
412
446
  return 1;
413
447
  }
414
- io.out(`${JSON.stringify({ ok: true, services: expectedNames.length + twinned.length + native.length, connections: infraConnections(declared, env) })}\n`);
448
+ io.out(`${JSON.stringify({ ok: true, services: expectedNames.length + twinned.length + native.length, connections: infraConnections(declared, env), ...backingFact('docker'), ...databaseFacts() })}\n`);
415
449
  }
416
450
  else {
417
451
  const result = await run(['down', '--remove-orphans']);
@@ -1,4 +1,5 @@
1
1
  import { type CoverageReport } from './covers.js';
2
+ import { type ProjectMigrationTool } from './project-inspect.js';
2
3
  import { type WorldConfig, type WorldServiceConfig } from './schema.js';
3
4
  /** How a twin service in the emitted world is reached by the app's traffic. */
4
5
  export type InitWiring =
@@ -111,6 +112,9 @@ export type InitPlan = {
111
112
  envSources: string[];
112
113
  /** empty example values read by production source — prominent warning, not vendor-coverage fact */
113
114
  bootRisks: InitBootRisk[];
115
+ /** migration tools found in the repository while the World manages a database and declares no step
116
+ * (runtime.database.prepare): reported, never written, and no part of the vendor-coverage verdict */
117
+ undeclaredMigrations: ProjectMigrationTool[];
114
118
  infra: InitInfraStub[];
115
119
  /** the services emitted into the private managed-infrastructure definition */
116
120
  compose: InitComposeService[];
package/dist/src/init.js CHANGED
@@ -36,11 +36,11 @@
36
36
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
37
37
  import { PROTOCOL_MAJOR, stateDirName } from '@volter/world-core/runtime';
38
38
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
39
- import { coverWorld, detectRepoVendors, envNameVendor, formatCoverageReport, injectorEnvNameForKey, injectorVendorKeysFor, isCredentialShapedEnvName, registryAcknowledgedReason, } from "./covers.js";
39
+ import { coverWorld, detectRepoVendors, envNameVendor, coverageRecord, formatCoverageReport, injectorEnvNameForKey, injectorVendorKeysFor, isCredentialShapedEnvName, registryAcknowledgedReason, } from "./covers.js";
40
40
  import { fakeEnvValue, isAppKeyName, isGoogleOAuthClientEnvName, isGoogleServiceAccountEnvName } from "./fixture-env.js";
41
41
  import { findInstalledPackage, packCli, packEntry, resolveCatalog } from "./catalog.js";
42
42
  import { CATALOG_INDEX_PACKAGE, defaults as catalogDefaults } from "./catalog-index.js";
43
- import { projectEnvReads, projectManifestDirs } from "./project-inspect.js";
43
+ import { projectEnvReads, projectManifestDirs, projectMigrationTools } from "./project-inspect.js";
44
44
  import { overlayEndpointEnv, packFacts } from "./pack-facts.js";
45
45
  import { VENDOR_SERVER_BACKINGS } from "./backings.js";
46
46
  import { APP_URL_ENV, DEFAULT_WORLD_SELECTION, ISSUE_ENV_PREFIX, MINT_ENV, worldConfigDocument, worldSelectionIncludes } from "./schema.js";
@@ -1047,6 +1047,9 @@ export function planWorldInit(name, repoPath, options = {}) {
1047
1047
  envRows,
1048
1048
  envSources,
1049
1049
  bootRisks: [...bootRisks].sort((a, b) => a.name.localeCompare(b.name)),
1050
+ // A migration belongs in the World's step, run once where the image is built. init can see the tool, not the step.
1051
+ undeclaredMigrations: existing?.databasePrepare !== undefined || !(compose.some((service) => service.kind === 'postgres' || service.kind === 'mysql') || existingIds.has('infrastructure'))
1052
+ ? [] : projectMigrationTools(repo),
1050
1053
  infra,
1051
1054
  compose,
1052
1055
  composePath: compose.length > 0 ? resolve(out, 'world.infrastructure.yml') : null,
@@ -1248,6 +1251,10 @@ export function initWorld(name, repoPath, options = {}) {
1248
1251
  // The proof was addressed BY PATH (so it reads the bytes just written, not the in-memory plan);
1249
1252
  // report it under the world's id, which is what every other verb takes.
1250
1253
  const coverage = { ...proof, world: plan.name };
1254
+ // The proof is kept in the config it was made against, beside the selection: the report printed below, as data.
1255
+ // Written after the proof because the proof reads the config's bytes; it reads nothing of `discovery.coverage`.
1256
+ plan.config.coverage = coverageRecord(coverage);
1257
+ writeFileSync(plan.configPath, `${JSON.stringify(worldConfigDocument(plan.config), null, 2)}\n`);
1251
1258
  const next = plan.inRepo && resolve(options.root ?? plan.repo) === plan.repo
1252
1259
  ? [
1253
1260
  'volter world up # starts the twins and loads the default data; the live env lands in .volter/world.env',
@@ -1329,6 +1336,17 @@ export function formatInitReport(result) {
1329
1336
  }
1330
1337
  lines.push(' Resolve these before expecting the app to listen. Vendor coverage can be green while the app rejects its own env.');
1331
1338
  }
1339
+ if (plan.undeclaredMigrations.length > 0) {
1340
+ lines.push('');
1341
+ lines.push(`MIGRATION TOOL FOUND, NO STEP DECLARED (${plan.undeclaredMigrations.length}):`);
1342
+ for (const found of plan.undeclaredMigrations)
1343
+ lines.push(` ${found.tool} — migrations at ${found.migrations} (schema ${found.schema})`);
1344
+ lines.push(' The World manages this app\'s database and declares no runtime.database.prepare, so the app would migrate it at');
1345
+ lines.push(' every start. Declare the step in the World: `command` (the migration as the app\'s image runs it, an argv), `cwd`');
1346
+ lines.push(' (the image path it runs in) and `inputs` (the schema and migrations, as paths under it). These are the IMAGE\'s');
1347
+ lines.push(' paths, which its Dockerfile decides and this repository does not state; an image build that reads the image');
1348
+ lines.push(' prints the step.');
1349
+ }
1332
1350
  if (plan.compose.length > 0) {
1333
1351
  lines.push('');
1334
1352
  lines.push('Infrastructure (owned by the World lifecycle):');
@@ -1,9 +1,23 @@
1
- import type { HostLaunch, InfraService } from './pglite-backing.js';
1
+ import { type HostLaunch, type InfraService } from './pglite-backing.js';
2
2
  /** The PostgreSQL tool directory this machine offers, or undefined: `VOLTER_WORLD_POSTGRES_BIN` as named, else the
3
3
  * directory of a `postgres` server on PATH, resolved through its links, holding the server's own tools beside it
4
4
  * (libpq's client tools on PATH name no server). Read from executable files; nothing is launched or installed
5
5
  * (architecture, managed infrastructure). */
6
6
  export declare function nativePostgresTools(env: NodeJS.ProcessEnv): string | undefined;
7
+ /** Where a World's prepared native cluster lives, beside its world.json, and its stamp (ADR 0018's layout). */
8
+ export declare function nativePreparedCluster(worldConfig: string): {
9
+ directory: string;
10
+ stamp: string;
11
+ };
12
+ /**
13
+ * `volter-world-infra prepare` for a World whose PostgreSQL is native (architecture, managed infrastructure; ADR
14
+ * 0018's phase): this lifecycle's own steps once, into `<World dir>/.volter-prepared/native-postgres-data`. initdb with
15
+ * up's arguments, the postmaster on the World's declared loopback port, the declared database, and a clean SIGINT stop (its
16
+ * shutdown checkpoint; no postmaster.pid left), then the stamp. Where the World declares runtime.database.prepare, the
17
+ * step runs against that postmaster before its stop, and a step that fails leaves no cluster. The image build runs it with the programs the tab
18
+ * runs, under the tab's kernel, so the cluster is the one a first boot there would write.
19
+ */
20
+ export declare function nativePrepare(service: InfraService, definition: string, worldConfig: string, launch?: HostLaunch): Promise<string>;
7
21
  export declare function nativePostgresUp(services: InfraService[], root: string, definition: string, launch: HostLaunch): Promise<void>;
8
22
  export declare function nativePostgresStatus(root: string): Promise<boolean>;
9
23
  export declare function nativePostgresDown(root: string): Promise<void>;