@rehearsal-db/core 1.1.0 → 1.3.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 (73) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/COMPATIBILITY.md +40 -0
  3. package/README.md +7 -3
  4. package/dist/package.json +4 -1
  5. package/dist/src/cli/arguments.d.mts +1 -0
  6. package/dist/src/cli/arguments.mjs +5 -0
  7. package/dist/src/cli/basic_command_handlers.mjs +6 -0
  8. package/dist/src/cli/cli_commands.mjs +2 -0
  9. package/dist/src/cli/editor_command_handlers.mjs +24 -0
  10. package/dist/src/cli/project_commands.d.mts +8 -0
  11. package/dist/src/cli/runtime_commands.d.mts +2 -19
  12. package/dist/src/cli/runtime_commands.mjs +3 -90
  13. package/dist/src/cli/runtime_stack_runner.d.mts +41 -0
  14. package/dist/src/cli/runtime_stack_runner.mjs +104 -0
  15. package/dist/src/cli/runtime_workload_admission.d.mts +8 -0
  16. package/dist/src/cli/runtime_workload_admission.mjs +22 -0
  17. package/dist/src/cli/source_commands.d.mts +8 -0
  18. package/dist/src/cli/workload_command_handlers.d.mts +2 -0
  19. package/dist/src/cli/workload_command_handlers.mjs +38 -0
  20. package/dist/src/project/configuration_contract.d.mts +5 -0
  21. package/dist/src/project/configuration_normalization.d.mts +4 -0
  22. package/dist/src/project/configuration_resolution.d.mts +4 -0
  23. package/dist/src/project/configuration_resource_policy.d.mts +4 -0
  24. package/dist/src/project/configuration_resource_policy.mjs +20 -1
  25. package/dist/src/project/configuration_schema.mjs +1 -0
  26. package/dist/src/runtime/migration_history.d.mts +5 -1
  27. package/dist/src/runtime/migration_history.mjs +24 -6
  28. package/dist/src/runtime/migration_impact.d.mts +125 -0
  29. package/dist/src/runtime/migration_impact.mjs +278 -0
  30. package/dist/src/runtime/migration_impact_catalog.d.mts +18 -0
  31. package/dist/src/runtime/migration_impact_catalog.mjs +116 -0
  32. package/dist/src/runtime/migration_impact_plan.d.mts +47 -0
  33. package/dist/src/runtime/migration_impact_plan.mjs +297 -0
  34. package/dist/src/runtime/migration_impact_queries.d.mts +22 -0
  35. package/dist/src/runtime/migration_impact_queries.mjs +270 -0
  36. package/dist/src/runtime/migration_insights.d.mts +137 -0
  37. package/dist/src/runtime/migration_insights.mjs +143 -0
  38. package/dist/src/runtime/migration_insights_input.d.mts +13 -0
  39. package/dist/src/runtime/migration_insights_input.mjs +153 -0
  40. package/dist/src/runtime/migration_sql_lexer.d.mts +14 -0
  41. package/dist/src/runtime/migration_sql_lexer.mjs +204 -0
  42. package/dist/src/runtime/migration_sql_scan.d.mts +16 -0
  43. package/dist/src/runtime/migration_sql_scan.mjs +369 -0
  44. package/dist/src/runtime/plan.d.mts +2 -1
  45. package/dist/src/runtime/plan.mjs +4 -4
  46. package/dist/src/runtime/topology.d.mts +1 -1
  47. package/dist/src/runtime/topology.mjs +2 -2
  48. package/dist/src/runtime/workload_queue.d.mts +39 -0
  49. package/dist/src/runtime/workload_queue.mjs +211 -0
  50. package/dist/src/runtime/workload_queue_location.d.mts +1 -0
  51. package/dist/src/runtime/workload_queue_location.mjs +43 -0
  52. package/dist/src/runtime/workload_queue_store.d.mts +27 -0
  53. package/dist/src/runtime/workload_queue_store.mjs +182 -0
  54. package/dist/src/shared/cancellation.mjs +2 -0
  55. package/dist/src/shared/held_path_ancestry.d.mts +7 -0
  56. package/dist/src/shared/held_path_ancestry.mjs +110 -0
  57. package/dist/src/shared/operation_guard.mjs +1 -0
  58. package/dist/src/targets/migration_impact_runtime.d.mts +22 -0
  59. package/dist/src/targets/migration_impact_runtime.mjs +367 -0
  60. package/docs/README.md +20 -7
  61. package/docs/adapters.md +10 -0
  62. package/docs/architecture.md +84 -1
  63. package/docs/commands.md +189 -34
  64. package/docs/configuration.md +56 -6
  65. package/docs/getting-started.md +1 -1
  66. package/docs/index.md +1 -0
  67. package/docs/releasing.md +15 -9
  68. package/docs/roadmap.md +27 -6
  69. package/docs/runtime-lifecycle.md +10 -0
  70. package/docs/stability.md +4 -4
  71. package/docs/updating.md +73 -0
  72. package/docs/vscode.md +23 -17
  73. package/package.json +4 -1
@@ -0,0 +1,367 @@
1
+ /** Private non-starting transport for one explicitly selected owned local runtime. */
2
+ import { constants } from "node:fs";
3
+ import { createHash } from "node:crypto";
4
+ import { lstat, open, realpath } from "node:fs/promises";
5
+ import { isAbsolute, join, relative, resolve } from "node:path";
6
+ import { isLocalDockerEndpoint } from "../runtime/local_docker_context.mjs";
7
+ import { diagnoseMigrationFailure } from "../runtime/migration_failure.mjs";
8
+ import { holdPathAncestry } from "../shared/held_path_ancestry.mjs";
9
+ import { runOwnedProcess } from "../shared/owned_process.mjs";
10
+ import { createCleanProcessEnvironment } from "../shared/process_environment.mjs";
11
+ import { assertDockerResourceLabels } from "./docker_resource_ownership.mjs";
12
+ export class MigrationImpactRuntimeError extends Error {
13
+ reason;
14
+ constructor(reason) {
15
+ super("The selected local runtime could not complete bounded read-only inspection; private output withheld.");
16
+ this.reason = reason;
17
+ this.name = "MigrationImpactRuntimeError";
18
+ }
19
+ }
20
+ const INSPECTION_BYTES = 65536;
21
+ const SQL_INPUT_BYTES = 262144;
22
+ const SQL_OUTPUT_BYTES = 131072;
23
+ function fail(reason) {
24
+ throw new MigrationImpactRuntimeError(reason);
25
+ }
26
+ const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
27
+ const object = (value) => {
28
+ if (!value || typeof value !== "object" || Array.isArray(value))
29
+ fail("runtime_unavailable");
30
+ return value;
31
+ };
32
+ const inside = (root, path) => {
33
+ const child = relative(root, path);
34
+ return (child !== "" &&
35
+ child !== ".." &&
36
+ !child.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) &&
37
+ !isAbsolute(child));
38
+ };
39
+ const CONTAINER_FORMAT = '{"id":{{json .Id}},"name":{{json .Name}},"image":{{json .Config.Image}},"imageId":{{json .Image}},"labels":{{json .Config.Labels}},"running":{{json .State.Running}},"ports":{{json .NetworkSettings.Ports}}}';
40
+ /** No lifecycle, adapter, source-reader, URL or credential-file access is permitted here. */
41
+ export async function openMigrationImpactRuntime(options) {
42
+ const { target, baseline } = options;
43
+ const signal = options.signal ?? AbortSignal.timeout(30000);
44
+ const run = options.runProcessImplementation ?? runOwnedProcess;
45
+ const cancelled = () => {
46
+ if (signal.aborted)
47
+ fail("cancelled");
48
+ };
49
+ cancelled();
50
+ const provider = target.config.runtime.target;
51
+ const projectId = target.config.runtime.projectId;
52
+ const postgres = provider === "postgresql" ? target.config.postgresql : null;
53
+ const databasePort = target.config.runtime.ports.database;
54
+ if (!["postgresql", "supabase"].includes(provider) ||
55
+ (provider === "postgresql" && !postgres))
56
+ fail("ownership_mismatch");
57
+ const dataSha256 = baseline.files["sanitized-data.ndjson"]?.sha256;
58
+ if (!/^[a-f0-9]{64}$/u.test(dataSha256 ?? "") ||
59
+ !/^\d{8}T\d{6}Z-[a-f0-9]{12}$/u.test(baseline.generationId))
60
+ fail("stale_runtime");
61
+ let root, receiptDirectory;
62
+ try {
63
+ root = await realpath(resolve(options.projectRoot));
64
+ const lexicalRoot = resolve(options.projectRoot), configured = resolve(target.paths.runtimeWorkdir);
65
+ receiptDirectory = inside(lexicalRoot, configured)
66
+ ? resolve(root, relative(lexicalRoot, configured))
67
+ : configured;
68
+ if (!inside(root, receiptDirectory) ||
69
+ (await realpath(receiptDirectory)) !== receiptDirectory)
70
+ fail("ownership_mismatch");
71
+ }
72
+ catch {
73
+ fail("ownership_mismatch");
74
+ }
75
+ const readReceipt = async () => {
76
+ cancelled();
77
+ let ancestry;
78
+ try {
79
+ ancestry = await holdPathAncestry(receiptDirectory);
80
+ await ancestry.assertUnchanged();
81
+ const path = join(ancestry.path, "baseline.json"), leaf = await lstat(path, { bigint: true });
82
+ if (!leaf.isFile() ||
83
+ leaf.isSymbolicLink() ||
84
+ leaf.size > BigInt(INSPECTION_BYTES) ||
85
+ (leaf.mode & 63n) !== 0n ||
86
+ (typeof process.getuid === "function" &&
87
+ leaf.uid !== BigInt(process.getuid())))
88
+ fail("stale_runtime");
89
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
90
+ try {
91
+ const before = await handle.stat({ bigint: true });
92
+ if (!before.isFile() ||
93
+ before.dev !== leaf.dev ||
94
+ before.ino !== leaf.ino ||
95
+ before.size !== leaf.size ||
96
+ before.mtimeNs !== leaf.mtimeNs ||
97
+ before.ctimeNs !== leaf.ctimeNs)
98
+ fail("stale_runtime");
99
+ const bytes = Buffer.alloc(Number(before.size) + 1);
100
+ let length = 0;
101
+ while (length < bytes.length) {
102
+ const next = await handle.read(bytes, length, bytes.length - length, length);
103
+ if (!next.bytesRead)
104
+ break;
105
+ length += next.bytesRead;
106
+ }
107
+ const after = await handle.stat({ bigint: true }), current = await lstat(path, { bigint: true });
108
+ if (BigInt(length) !== before.size ||
109
+ !current.isFile() ||
110
+ current.isSymbolicLink() ||
111
+ [after, current].some((stat) => stat.dev !== before.dev ||
112
+ stat.ino !== before.ino ||
113
+ stat.size !== before.size ||
114
+ stat.mtimeNs !== before.mtimeNs ||
115
+ stat.ctimeNs !== before.ctimeNs))
116
+ fail("stale_runtime");
117
+ await ancestry.assertUnchanged();
118
+ const contents = bytes.subarray(0, length);
119
+ const marker = object(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(contents)));
120
+ if (marker.target !== provider ||
121
+ marker.generationId !== baseline.generationId ||
122
+ marker.dataSha256 !== dataSha256 ||
123
+ (postgres
124
+ ? marker.formatVersion !== 1 ||
125
+ marker.image !== postgres.image ||
126
+ typeof marker.databasePassword !== "string" ||
127
+ marker.databasePassword.length < 32
128
+ : marker.formatVersion !== baseline.formatVersion ||
129
+ marker.schemaSha256 !==
130
+ (baseline.files["production-schema.sql"]?.sha256 ?? null)))
131
+ fail("stale_runtime");
132
+ return {
133
+ sha256: digest(contents),
134
+ inode: `${before.dev}:${before.ino}:${before.ctimeNs}:${before.mtimeNs}`,
135
+ };
136
+ }
137
+ finally {
138
+ await handle.close();
139
+ }
140
+ }
141
+ catch (error) {
142
+ if (error instanceof MigrationImpactRuntimeError)
143
+ throw error;
144
+ fail("stale_runtime");
145
+ }
146
+ finally {
147
+ try {
148
+ await ancestry?.close();
149
+ }
150
+ catch {
151
+ cancelled();
152
+ fail("stale_runtime");
153
+ }
154
+ }
155
+ };
156
+ const initialReceipt = await readReceipt();
157
+ let endpoint;
158
+ const invoke = async (args, input) => {
159
+ cancelled();
160
+ const maximum = input === undefined ? INSPECTION_BYTES : SQL_OUTPUT_BYTES;
161
+ try {
162
+ const result = await run({
163
+ command: "docker",
164
+ args,
165
+ cwd: root,
166
+ env: createCleanProcessEnvironment({
167
+ overrides: { DOCKER_HOST: endpoint },
168
+ }),
169
+ signal,
170
+ stdin: "ignore",
171
+ ...(input === undefined ? {} : { input }),
172
+ maxOutputBytes: maximum,
173
+ cancellationGraceMs: 1000,
174
+ });
175
+ cancelled();
176
+ if (typeof result.stdout !== "string" ||
177
+ typeof result.stderr !== "string" ||
178
+ !Number.isSafeInteger(result.stdoutByteCount) ||
179
+ !Number.isSafeInteger(result.stderrByteCount) ||
180
+ result.stdoutByteCount < Buffer.byteLength(result.stdout) ||
181
+ result.stderrByteCount < Buffer.byteLength(result.stderr) ||
182
+ result.stdoutByteCount > maximum ||
183
+ result.stderrByteCount > INSPECTION_BYTES ||
184
+ (input === undefined &&
185
+ result.stdoutByteCount + result.stderrByteCount > INSPECTION_BYTES))
186
+ fail(input === undefined ? "runtime_unavailable" : "query_failed");
187
+ if (result.error || result.signal || result.status !== 0) {
188
+ if (input !== undefined) {
189
+ const code = diagnoseMigrationFailure({
190
+ provider: "postgresql",
191
+ stderr: result.stderr,
192
+ }).sqlState;
193
+ if (code === "55P03")
194
+ fail("lock_timeout");
195
+ if (code === "57014")
196
+ fail("statement_timeout");
197
+ if (code === "25P04")
198
+ fail("transaction_timeout");
199
+ }
200
+ fail(input === undefined ? "runtime_unavailable" : "query_failed");
201
+ }
202
+ return result.stdout;
203
+ }
204
+ catch (error) {
205
+ cancelled();
206
+ if (error instanceof MigrationImpactRuntimeError)
207
+ throw error;
208
+ fail(input === undefined ? "runtime_unavailable" : "query_failed");
209
+ }
210
+ };
211
+ const parse = (text) => {
212
+ try {
213
+ return JSON.parse(text);
214
+ }
215
+ catch {
216
+ fail("runtime_unavailable");
217
+ }
218
+ };
219
+ const context = (await invoke(["context", "show"])).trim();
220
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/u.test(context))
221
+ fail("ownership_mismatch");
222
+ const selected = parse(await invoke([
223
+ "context",
224
+ "inspect",
225
+ "--format",
226
+ "{{json .Endpoints.docker.Host}}",
227
+ context,
228
+ ]));
229
+ if (!isLocalDockerEndpoint(selected))
230
+ fail("ownership_mismatch");
231
+ endpoint = selected;
232
+ const engineId = async () => {
233
+ const value = parse(await invoke(["info", "--format", "{{json .ID}}"]));
234
+ if (typeof value !== "string" || !/^[a-zA-Z0-9:-]{1,128}$/u.test(value))
235
+ fail("runtime_unavailable");
236
+ return value;
237
+ };
238
+ const engine = await engineId();
239
+ const expectedName = postgres
240
+ ? `${projectId}-postgresql`
241
+ : `supabase_db_${projectId}`;
242
+ const inspect = async (id) => {
243
+ const value = object(parse(await invoke([
244
+ "container",
245
+ "inspect",
246
+ "--format",
247
+ CONTAINER_FORMAT,
248
+ id,
249
+ ])));
250
+ if (typeof value.id !== "string" ||
251
+ !/^[a-f0-9]{64}$/u.test(value.id) ||
252
+ value.name !== `/${expectedName}` ||
253
+ value.running !== true ||
254
+ typeof value.imageId !== "string" ||
255
+ !/^sha256:[a-f0-9]{64}$/u.test(value.imageId))
256
+ fail("runtime_unavailable");
257
+ try {
258
+ assertDockerResourceLabels({
259
+ labels: value.labels,
260
+ expectedLabel: postgres
261
+ ? "com.rehearsal-db.project"
262
+ : "com.supabase.cli.project",
263
+ projectId,
264
+ });
265
+ }
266
+ catch {
267
+ fail("ownership_mismatch");
268
+ }
269
+ if (postgres) {
270
+ const ports = object(value.ports), mappings = ports["5432/tcp"];
271
+ if (value.image !== postgres.image ||
272
+ !Array.isArray(mappings) ||
273
+ mappings.length !== 1)
274
+ fail("ownership_mismatch");
275
+ const mapping = object(mappings[0]);
276
+ if (mapping.HostIp !== "127.0.0.1" ||
277
+ mapping.HostPort !== String(databasePort))
278
+ fail("ownership_mismatch");
279
+ }
280
+ return { id: value.id, imageId: value.imageId };
281
+ };
282
+ const container = await inspect(expectedName);
283
+ const assertCurrent = async () => {
284
+ cancelled();
285
+ const currentReceipt = await readReceipt();
286
+ if (currentReceipt.sha256 !== initialReceipt.sha256 ||
287
+ currentReceipt.inode !== initialReceipt.inode)
288
+ fail("stale_runtime");
289
+ if ((await engineId()) !== engine)
290
+ fail("stale_runtime");
291
+ const current = await inspect(container.id);
292
+ if (current.id !== container.id || current.imageId !== container.imageId)
293
+ fail("stale_runtime");
294
+ const finalReceipt = await readReceipt();
295
+ if (finalReceipt.sha256 !== initialReceipt.sha256 ||
296
+ finalReceipt.inode !== initialReceipt.inode)
297
+ fail("stale_runtime");
298
+ };
299
+ await assertCurrent();
300
+ return Object.freeze({
301
+ identitySha256: digest(JSON.stringify({
302
+ endpoint,
303
+ engine,
304
+ container,
305
+ receipt: initialReceipt.sha256,
306
+ provider,
307
+ projectId,
308
+ generation: baseline.generationId,
309
+ })),
310
+ assertCurrent,
311
+ runSql: async (input) => {
312
+ // Compile the remaining server deadline only after the final potentially
313
+ // slow Docker/receipt preflight. Never rewrite a caller's SQL text.
314
+ if (typeof input === "function")
315
+ await assertCurrent();
316
+ const sql = typeof input === "function" ? input() : input;
317
+ if (typeof sql !== "string" ||
318
+ Buffer.byteLength(sql) > SQL_INPUT_BYTES ||
319
+ sql.length === 0 ||
320
+ sql.includes("\0"))
321
+ fail("query_failed");
322
+ if (typeof input !== "function")
323
+ await assertCurrent();
324
+ let result = "", failure;
325
+ try {
326
+ // Docker exec inherits container variables unless env -i clears them;
327
+ // force a local socket so PGHOSTADDR/service/options cannot redirect it.
328
+ result = await invoke([
329
+ "exec",
330
+ "--interactive",
331
+ container.id,
332
+ "/usr/bin/env",
333
+ "-i",
334
+ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
335
+ "PGSSLMODE=disable",
336
+ "PGPASSFILE=/dev/null",
337
+ "psql",
338
+ "--no-psqlrc",
339
+ "--no-password",
340
+ "--host=/var/run/postgresql",
341
+ "--port=5432",
342
+ "--quiet",
343
+ "--no-align",
344
+ "--tuples-only",
345
+ "--set",
346
+ "ON_ERROR_STOP=1",
347
+ "--set",
348
+ "VERBOSITY=sqlstate",
349
+ "--username",
350
+ // Supabase's application-facing postgres role is NOSUPERUSER.
351
+ // The fixed disk bound and namespace lock require its built-in
352
+ // local administrator; no role changes or credentials are needed.
353
+ postgres?.user ?? "supabase_admin",
354
+ "--dbname",
355
+ postgres?.database ?? "postgres",
356
+ ], sql);
357
+ }
358
+ catch (error) {
359
+ failure = error;
360
+ }
361
+ await assertCurrent();
362
+ if (failure)
363
+ throw failure;
364
+ return result;
365
+ },
366
+ });
367
+ }
package/docs/README.md CHANGED
@@ -10,17 +10,26 @@ applies only the exact migrations you approve, and runs your application's own p
10
10
  You do not need to read this documentation from top to bottom. Choose what you want to
11
11
  accomplish and follow that path.
12
12
 
13
- ## Start here
13
+ ## Getting started
14
+
15
+ Install Rehearsal, rehearse a migration, understand the result and fix a failure.
14
16
 
15
17
  | Your goal | Best next page |
16
18
  | ----------------------------------- | ------------------------------------- |
17
19
  | Add Rehearsal to a project | [Getting started](getting-started.md) |
18
20
  | Use the visual database workbench | [VS Code extension](vscode.md) |
19
21
  | Try it in a disposable project | [Hands-on tutorial](tutorial.md) |
22
+ | Update an existing installation | [Releases and updates](updating.md) |
20
23
  | Fix a setup or runtime problem | [Troubleshooting](troubleshooting.md) |
21
24
  | Browse the friendlier web-docs home | [Documentation home](index.md) |
22
25
 
23
- ## Work safely
26
+ ## User reference
27
+
28
+ Look up only what you need for your next decision. Start with
29
+ [supported environments](https://github.com/Ddupasquier/rehearsal-db/blob/main/COMPATIBILITY.md),
30
+ [configuration](configuration.md) or [troubleshooting](troubleshooting.md).
31
+
32
+ ### Data safety
24
33
 
25
34
  - [Baselines](baselines.md) — the verified starting copy
26
35
  - [Sanitization](sanitization.md) — what may enter that copy
@@ -28,7 +37,7 @@ accomplish and follow that path.
28
37
  - [Security model](security-model.md) — the independent safety barriers
29
38
  - [Standalone workflow](standalone-workflow.md) — the complete declarative contract
30
39
 
31
- ## Look something up
40
+ ### Commands and settings
32
41
 
33
42
  - [CLI commands](commands.md)
34
43
  - [Configuration](configuration.md)
@@ -37,13 +46,17 @@ accomplish and follow that path.
37
46
  - [Runtime and identity policies](runtime-policies.md)
38
47
  - [Database adapters](adapters.md)
39
48
  - [Glossary](glossary.md)
49
+ - [Roadmap](roadmap.md)
40
50
 
41
- ## Follow the project
51
+ ## Maintainers
52
+
53
+ These pages explain developing and publishing Rehearsal itself, not using it in
54
+ your application. Installing or updating does not require the publisher checklist.
42
55
 
43
- - [Roadmap](roadmap.md)
44
- - [Stability and release verification](stability.md)
45
56
  - [Repository architecture](architecture.md)
46
- - [Release process](releasing.md)
57
+ - [Building and testing](https://github.com/Ddupasquier/rehearsal-db/blob/main/CONTRIBUTING.md)
58
+ - [Release verification](stability.md)
59
+ - [Maintainers: publishing Rehearsal](releasing.md)
47
60
 
48
61
  To preview the styled documentation locally, run `npm run docs:preview`, then open the
49
62
  address it prints.
package/docs/adapters.md CHANGED
@@ -35,3 +35,13 @@ adapter responsibility.
35
35
  Keep adapters small, deterministic, tested, and visibly project-owned. If a behavior is
36
36
  generic across unrelated projects, propose it for the engine instead of copying it into
37
37
  every adapter.
38
+
39
+ ## Migration-tool histories are a separate contract
40
+
41
+ Project runtime hooks do not add Prisma, Drizzle, Knex, Django or Alembic support.
42
+ The current timestamped-SQL inventory and Rehearsal/Supabase migration ledger must
43
+ not be presented as native compatibility with those tools. Do not flatten native
44
+ migration directories, rewrite an applied ledger or use a schema-push shortcut to
45
+ fit this adapter API. Versioned native history, exact review and owned-target
46
+ execution belong to the separate core integration tracked by
47
+ [issue #174](https://github.com/Ddupasquier/rehearsal-db/issues/174).
@@ -39,6 +39,23 @@ rehearsal-db/
39
39
  - Public consumers use only the paths declared in `package.json#exports`.
40
40
  - Unit tests mirror their source domain. Cross-domain workflows belong in
41
41
  `tests/integration/`; durable public promises belong in `tests/contracts/`.
42
+ - `tests/helpers/public_cli.mjs` bounds setup-test child output and projects only
43
+ fixed failure categories, exit/signal metadata and recognized setup refusal kinds.
44
+ It never retains raw child errors, argv, provider text or output in failure reports.
45
+ An unrecognized failure stays unclassified; a later green test does not establish
46
+ the cause of a historical failure. This test helper is not shipped product code.
47
+
48
+ ## Documentation audiences
49
+
50
+ Site navigation separates Getting started, User reference and Maintainers. Installation,
51
+ rehearsal, results and failure recovery belong in the first journey; configuration,
52
+ supported environments, troubleshooting and data safety belong in reference. Keep
53
+ deletion, production-access and unsupported-behavior warnings visible to users.
54
+
55
+ `docs/updating.md` answers release changes, compatibility, updating, breaking changes
56
+ and origin verification with links to deeper owners. Build, test and publishing details
57
+ belong in maintainer guidance, not installation prerequisites. `docs/releasing.md` owns
58
+ current publishing controls; historical incidents stay in release and issue history.
42
59
 
43
60
  ## CLI modules
44
61
 
@@ -60,9 +77,19 @@ typed registry and produce the same result contract:
60
77
  `update_advisory.mts` bounds public release metadata, private preferences and
61
78
  installation-origin checks. Neither participates in passive JSON inspection.
62
79
  - `runtime_commands.mts` coordinates local runtimes and project proofs.
80
+ - `runtime_stack_runner.mts` selects configured targets, binds their reviewed engine
81
+ and configuration identities, executes the serial stack and retains receipt-bound
82
+ cleanup. `runtime_workload_admission.mts` applies optional lifecycle admission before
83
+ that work; application proofs and interactive sessions are deliberately outside it.
84
+ - `workload_command_handlers.mts` exposes local queue inspection and exact dead-holder
85
+ metadata recovery without a project lock or Docker. `runtime/workload_queue.mts`
86
+ owns cooperative lifecycle admission; its store owns bounded private atomic tickets,
87
+ and its location helper binds the local user, boot and PID namespace independently
88
+ of per-command temporary directories. No background scheduler is installed.
63
89
  - `runtime_planning.mts` owns read-only stack plans and exact candidate summaries.
64
90
  - `editor_command_handlers.mts` exposes bounded migration and configuration review
65
- contracts plus digest-bound, non-destructive stale-environment stop review.
91
+ contracts plus digest-bound, non-destructive stale-environment stop review. It also
92
+ keeps impact preview/measurement dispatch thin; aggregate policy stays in runtime.
66
93
  - `source_commands.mts` coordinates approved source access and refresh.
67
94
  - `renderers.mts` formats human-readable results without changing state.
68
95
  - `terminal.mts` owns prompts, colors, and terminal exit behavior.
@@ -98,6 +125,62 @@ handler-family module at 350 lines or fewer. A new command belongs in the narrow
98
125
  existing family, or in a new focused family when it represents a genuinely separate
99
126
  responsibility.
100
127
 
128
+ Static Migration Insights has a separate additive contract from migration-plan v1 and
129
+ post-failure SQLSTATE diagnosis. `runtime/migration_insights_input.mts` owns bounded
130
+ regular-file reads and private exact-input text; the existing inventory, baseline-prefix
131
+ and topology owners still calculate the candidate digest. Repeated shared-directory
132
+ inputs must keep the same raw hash; findings retrieve text only by its expected hash.
133
+ `shared/held_path_ancestry.mts` retains canonical directory identities and timestamps
134
+ to refuse detected concurrent ancestry changes. This is not atomic `openat` resolution
135
+ or a filesystem sandbox; leaf checks and existing workspace trust remain necessary.
136
+ `migration_sql_lexer.mts` and
137
+ `migration_sql_scan.mts` own bounded lexical positions and fixed review hints, not SQL
138
+ validation. `migration_insights.mts` alone projects redacted public findings and explicit
139
+ coverage/unknown states. The editor consumes that contract; it must not reimplement SQL
140
+ analysis or infer runtime measurements from static warnings.
141
+
142
+ Migration impact is a separate optional contract, not a new rehearsal gate.
143
+ `runtime/migration_impact_plan.mts` recognizes only bounded simple SQL forms and retains
144
+ private relation/column specifications; it does not execute migrations or claim SQL
145
+ validity. `migration_impact_queries.mts` validates those specifications and generates
146
+ fixed, identifier-quoted aggregate queries. `migration_impact_catalog.mts` owns the
147
+ closed native index, statistics and operator-family predicates. Metadata checks refuse unsupported
148
+ table, type and operator semantics; public output contains count strings or explicit
149
+ unknowns, never those private specifications, SQL, object names or row contents.
150
+ `migration_impact.mts` binds preview and measurement reports to the whole candidate
151
+ digest, selected target, immutable baseline and exact input bytes. It rechecks the
152
+ binding after measurement and never interprets current counts as sequential migration
153
+ simulation or a production prediction.
154
+
155
+ `targets/migration_impact_runtime.mts` owns access to an already-running, verified,
156
+ ownership-proven local sandbox and its unchanged identity. It does not start/repair
157
+ an engine, reset data or apply migrations. Measurement uses one read-only repeatable
158
+ read count transaction with bounded locks, statements, output and total duration.
159
+ The environment-scrubbed client connects only through the proven owned container's
160
+ fixed local socket. PostgreSQL uses its configured local owner; Supabase uses its
161
+ built-in `supabase_admin`, not the restricted application-facing `postgres` role.
162
+ The fixed temporary-file bound and namespace-catalog lock require that local
163
+ administrator. No grants, role promotion, password access, network connection or
164
+ caller-selected SQL is added; the same read-only compiler and metadata refusals apply.
165
+ A separate read-only catalog preflight retains relation OIDs; globally ordered
166
+ `SHARE ROW EXCLUSIVE` locks precede the first repeatable-read snapshot, preventing
167
+ index/statistics DDL from outrunning its metadata checks. Counts recheck the retained
168
+ OIDs and supported semantics after locking. A concurrent name-to-view swap can
169
+ transiently lock view dependencies within the selected sandbox, but cannot make the
170
+ compiler execute that view query or return its counts. This is not atomic name/OID
171
+ resolution or a sandbox for a compromised database server. The private transport
172
+ also takes a bounded database-local namespace-catalog `SHARE` lock before relation
173
+ name resolution, preventing schema rename/swap during checks and aggregates.
174
+ This can temporarily block schema-catalog changes anywhere in that selected
175
+ disposable database; it does not change another database or acquire a cluster-wide lock.
176
+ The transport invokes the script compiler after final identity preflight so a slow check cannot
177
+ leave an earlier, larger server deadline embedded in the submitted script. The CLI
178
+ classifies `measure impact` for the existing project-operation lock and cancellation
179
+ owner despite the SQL being read-only; operation metadata/history may be written.
180
+ `inspect impact` is an offline preview. Both require one explicit configured target;
181
+ measurement additionally requires the exact whole-plan confirmation. Neither command
182
+ is a prerequisite for ordinary `run` or `migrate`, and both remain free CLI features.
183
+
101
184
  ## Configuration pipeline
102
185
 
103
186
  The stable `project/configuration.mts` facade keeps existing imports compatible while