@rehearsal-db/core 1.0.1 → 1.1.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 (56) hide show
  1. package/CHANGELOG.md +62 -3
  2. package/COMPATIBILITY.md +10 -4
  3. package/README.md +2 -2
  4. package/dist/package.json +1 -1
  5. package/dist/src/cli/failure_output.d.mts +3 -0
  6. package/dist/src/cli/failure_output.mjs +48 -0
  7. package/dist/src/cli/rehearsal.mjs +3 -2
  8. package/dist/src/cli/runtime_action_handlers.mjs +11 -4
  9. package/dist/src/cli/runtime_commands.mjs +21 -7
  10. package/dist/src/cli/runtime_failure_summary.d.mts +1 -0
  11. package/dist/src/cli/runtime_failure_summary.mjs +30 -4
  12. package/dist/src/project/configuration_contract.d.mts +1 -0
  13. package/dist/src/project/configuration_normalization.mjs +2 -1
  14. package/dist/src/project/configuration_rendering.mjs +3 -2
  15. package/dist/src/project/configuration_resolution.mjs +0 -2
  16. package/dist/src/project/project_detection.d.mts +2 -0
  17. package/dist/src/project/project_detection.mjs +1 -0
  18. package/dist/src/project/runtime_project_identity.d.mts +5 -0
  19. package/dist/src/project/runtime_project_identity.mjs +41 -0
  20. package/dist/src/project/setup.mjs +3 -1
  21. package/dist/src/runtime/migration_failure.d.mts +28 -0
  22. package/dist/src/runtime/migration_failure.mjs +197 -0
  23. package/dist/src/runtime/migration_failure_source.d.mts +16 -0
  24. package/dist/src/runtime/migration_failure_source.mjs +68 -0
  25. package/dist/src/runtime/policy.d.mts +2 -0
  26. package/dist/src/runtime/policy.mjs +13 -1
  27. package/dist/src/runtime/privileges.d.mts +25 -0
  28. package/dist/src/runtime/privileges.mjs +198 -0
  29. package/dist/src/shared/diagnostics.mjs +5 -0
  30. package/dist/src/shared/operation_guard.d.mts +4 -1
  31. package/dist/src/shared/operation_guard.mjs +18 -13
  32. package/dist/src/shared/operation_progress.mjs +35 -1
  33. package/dist/src/shared/operation_progress_storage.d.mts +27 -6
  34. package/dist/src/shared/operation_progress_storage.mjs +65 -9
  35. package/dist/src/targets/port_start_failure.d.mts +15 -0
  36. package/dist/src/targets/port_start_failure.mjs +46 -0
  37. package/dist/src/targets/postgresql.mjs +8 -7
  38. package/dist/src/targets/postgresql_process.d.mts +8 -1
  39. package/dist/src/targets/postgresql_process.mjs +46 -2
  40. package/dist/src/targets/supabase.mjs +1 -0
  41. package/dist/src/targets/supabase_process.mjs +2 -1
  42. package/dist/src/targets/supabase_start_failure.mjs +5 -1
  43. package/docs/README.md +1 -1
  44. package/docs/architecture.md +32 -2
  45. package/docs/commands.md +35 -0
  46. package/docs/configuration.md +19 -4
  47. package/docs/getting-started.md +4 -4
  48. package/docs/releasing.md +4 -2
  49. package/docs/roadmap.md +30 -9
  50. package/docs/runtime-lifecycle.md +5 -4
  51. package/docs/runtime-policies.md +145 -0
  52. package/docs/stability.md +8 -4
  53. package/docs/troubleshooting.md +54 -2
  54. package/docs/tutorial.md +93 -8
  55. package/docs/vscode.md +48 -18
  56. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,10 +1,66 @@
1
1
  # Changelog
2
2
 
3
- All notable Rehearsal package changes will be documented here. The format follows Keep
4
- a Changelog, and versions will follow Semantic Versioning after the package exists.
3
+ All notable Rehearsal package changes are documented here. The format follows Keep
4
+ a Changelog, and published versions follow Semantic Versioning.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.1.0] - 2026-10-21
9
+
10
+ ### Added
11
+
12
+ - Optional read-only runtime privilege expectations for reviewed schemas, tables,
13
+ sequences and exact function signatures. Required and forbidden effective access is
14
+ separate from RLS, includes inherited and column-only permissions, and never repairs
15
+ grants automatically. Existing policies without this field keep their behavior.
16
+ - Document explicit least-privilege Data API grants, forward-only correction and
17
+ positive/negative API proofs for Supabase's October 30 opt-in table-exposure change.
18
+
19
+ ### Fixed
20
+
21
+ - Record the Supabase provider in newly written runtime receipts, so inventory can
22
+ distinguish recorded format/provider compatibility from unknown legacy provenance.
23
+ Older receipts remain unknown rather than being silently rewritten or reverified.
24
+ - Bound newly generated Supabase project IDs to the provider's 40-character limit
25
+ with a deterministic suffix. Preserve valid existing identities; reject invalid
26
+ configured IDs without renaming or adopting resources.
27
+ - Align OS setup helpers with core's Node.js 22, 24 and 26 support. macOS/Linux
28
+ helpers accept an explicit `REHEARSAL_NODE_PATH`, and missing-Node guidance points
29
+ to version-manager selection or the official installer without installing an OS tool.
30
+ - Update compatible older CLI installations to the helper's exact current target
31
+ instead of treating every 1.0.1-or-newer installation as already current.
32
+
33
+ ## [1.0.2] - 2026-10-20
34
+
35
+ ### Fixed
36
+
37
+ - Explain recognized PostgreSQL/Supabase migration failures with bounded native
38
+ SQLSTATE evidence and safe syntax, schema, constraint, permission, connectivity
39
+ and timeout guidance. Unsupported or ambiguous evidence remains unknown; raw
40
+ SQL, provider details, credentials and private rows are not exposed.
41
+ - Preserve the original diagnosis across cleanup and bounded operation history so
42
+ the CLI and editor can show the same failure after refresh. Failed operations
43
+ never claim verification or unconditional rollback success.
44
+ - Optionally identify the exact approved PostgreSQL input file by hash, without
45
+ inventing a failing SQL origin, line or column. Reject invalid UTF-8 before
46
+ execution rather than submitting replacement-decoded bytes.
47
+ - Point recognized bind failures at the configured local ports, including safe
48
+ Windows/WSL exclusion-inspection guidance. Do not infer a reservation from every
49
+ denial or suggest terminating unknown listeners.
50
+ - Keep database-only command guidance separate from optional application launch.
51
+
52
+ ### Documentation and distribution
53
+
54
+ - Add a main-tutorial existing-row constraint failure, reviewed backfill/fix and
55
+ verification sequence, plus a same-project VS Code handoff and safe local-copy
56
+ recovery for unsupported external npm links.
57
+ - Pair with Starter 0.1.22 for actionable persistent failures, safe exact-byte
58
+ source opening, distinct command/database states and truthful recorded proof.
59
+ - Refresh the separate macOS, Linux and Windows/WSL 2 setup bundles to exact core
60
+ 1.0.2. Native Windows database execution remains unsupported.
61
+
62
+ ## [1.0.1] - 2026-10-19
63
+
8
64
  ### Platform compatibility
9
65
 
10
66
  - Keep macOS Intel/Apple Silicon and Linux execution distinct from Windows desktop
@@ -784,7 +840,10 @@ not unattended cleanup, and container age is not proof of user inactivity.
784
840
  publication uses short-lived trusted OIDC, and every release tag must already exist on
785
841
  protected `main`.
786
842
 
787
- [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.0...HEAD
843
+ [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.1.0...HEAD
844
+ [1.1.0]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.2...v1.1.0
845
+ [1.0.2]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.1...v1.0.2
846
+ [1.0.1]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.0...v1.0.1
788
847
  [1.0.0]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.10...v1.0.0
789
848
  [0.1.0-rc.10]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.9...v0.1.0-rc.10
790
849
  [0.1.0-rc.9]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.8...v0.1.0-rc.9
package/COMPATIBILITY.md CHANGED
@@ -26,12 +26,18 @@ The first `1.0.0` release requires external beta evidence, a support policy and
26
26
  settled public API described in [the stability policy](docs/stability.md). Supporting
27
27
  additional database families, package managers or operating systems is not implied.
28
28
 
29
- Rehearsal supports maintained even-numbered Node.js release lines that pass its CI
30
- matrix. Rehearsal 1.0 supports Node.js 22, 24, and 26; Node.js 24 LTS is the recommended
31
- default and the release-build runtime. End-of-life and odd-numbered Node.js releases are
32
- not supported. A future release may remove a Node.js line after its upstream end-of-life
29
+ Rehearsal supports the maintained Node.js release lines verified by its existing CI
30
+ matrix: Node.js 22, 24, and 26. Node.js 24 LTS is the recommended default and the
31
+ release-build runtime. End-of-life and unverified future release lines are not
32
+ supported. A future release may remove a Node.js line after its upstream end-of-life
33
33
  date, with that change called out in the changelog.
34
34
 
35
+ Core 1.1.0's OS helpers accept the same three lines. A terminal or desktop launcher
36
+ that cannot find Node does not mean an installed version is unsupported. Select it
37
+ through the project's existing version manager or use the helper's explicit executable
38
+ path; see [OS setup](installers/README.md). Helpers never install Node or switch a
39
+ project's runtime automatically.
40
+
35
41
  ## Operating systems and execution location
36
42
 
37
43
  Core 1.0.1's platform boundary distinguishes the computer running VS Code from the
package/README.md CHANGED
@@ -47,7 +47,7 @@ it through a reviewed privacy policy, and retire the access again. It is never p
47
47
  production deployment tool.
48
48
 
49
49
  The CLI is free and MIT-licensed, with stable releases on
50
- [npm](https://www.npmjs.com/package/@rehearsal-db/core). This guide targets **1.0.1**;
50
+ [npm](https://www.npmjs.com/package/@rehearsal-db/core). This guide targets **1.1.0**;
51
51
  install only a version actually published there. Prefer a visual workflow?
52
52
  [Install the free VS Code Starter preview](docs/vscode.md). Its included features are
53
53
  free indefinitely for personal or commercial development—no account, card, or trial.
@@ -71,7 +71,7 @@ your migration is intended to change.
71
71
 
72
72
  Prefer OS-specific files? Use the [Windows / WSL 2](installers/windows/README.md),
73
73
  [macOS](installers/macos/README.md) or [Linux](installers/linux/README.md) setup folder.
74
- These readable helpers require existing Node.js 24 or 26 and check prerequisites by
74
+ These readable helpers accept existing Node.js 22, 24 or 26 and check prerequisites by
75
75
  default; installation is an explicit option. They do not install OS tools or start a
76
76
  database. See [their exact behavior and release availability](installers/README.md).
77
77
 
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehearsal-db/core",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "Catch PostgreSQL and Supabase migrations that pass on empty databases but fail on real data—before deploy.",
6
6
  "repository": {
@@ -0,0 +1,3 @@
1
+ /** Preserve fixed original migration guidance without masking aggregate cleanup failure. */
2
+ import { RehearsalError, type RehearsalErrorOverrides } from "../shared/diagnostics.mjs";
3
+ export declare const normalizeCliFailure: (error: unknown, overrides?: RehearsalErrorOverrides) => RehearsalError;
@@ -0,0 +1,48 @@
1
+ /** Preserve fixed original migration guidance without masking aggregate cleanup failure. */
2
+ import { RehearsalError, normalizeRehearsalError, } from "../shared/diagnostics.mjs";
3
+ import { parseMigrationFailureDiagnostic, } from "../runtime/migration_failure.mjs";
4
+ const aggregateMigrationFailure = (error) => {
5
+ try {
6
+ let original = error;
7
+ for (let depth = 0; depth <= 4; depth += 1) {
8
+ if (original instanceof AggregateError) {
9
+ if (depth === 4 || !Array.isArray(original.errors))
10
+ return undefined;
11
+ original = original.errors[0];
12
+ continue;
13
+ }
14
+ if (original instanceof RehearsalError &&
15
+ original.category !== "operation_cancelled" &&
16
+ original.context !== null &&
17
+ typeof original.context === "object" &&
18
+ !Array.isArray(original.context) &&
19
+ "migrationFailure" in original.context)
20
+ return parseMigrationFailureDiagnostic(original.context.migrationFailure);
21
+ return undefined;
22
+ }
23
+ }
24
+ catch {
25
+ /* Untrusted metadata is not evidence; retain the aggregate's existing failure. */
26
+ }
27
+ return undefined;
28
+ };
29
+ export const normalizeCliFailure = (error, overrides = {}) => {
30
+ const failure = normalizeRehearsalError(error, overrides);
31
+ if (!(error instanceof AggregateError))
32
+ return failure;
33
+ const migrationFailure = aggregateMigrationFailure(error);
34
+ if (migrationFailure === undefined)
35
+ return failure;
36
+ // Keep outer category/exit and cleanup warning. Project no original/sibling text.
37
+ return new RehearsalError({
38
+ category: failure.category,
39
+ code: failure.code,
40
+ message: failure.message,
41
+ expected: failure.expected,
42
+ actual: failure.actual,
43
+ context: { migrationFailure },
44
+ ...(failure.refused === undefined ? {} : { refused: failure.refused }),
45
+ suggestions: [...migrationFailure.suggestions, ...failure.suggestions],
46
+ cause: failure.cause,
47
+ });
48
+ };
@@ -8,13 +8,14 @@ import { join } from "node:path";
8
8
  import { performance } from "node:perf_hooks";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import * as prompts from "@clack/prompts";
11
- import { RehearsalError, normalizeRehearsalError, renderHumanError, serializeRehearsalError, } from "../shared/diagnostics.mjs";
11
+ import { RehearsalError, renderHumanError, serializeRehearsalError, } from "../shared/diagnostics.mjs";
12
12
  import { acquireProjectOperation, assertInstalledPackageFingerprint, createInstalledPackageFingerprint, } from "../shared/operation_guard.mjs";
13
13
  import { commandMutatesProject, parseArguments, resetGuidedFlags, } from "./arguments.mjs";
14
14
  import { usage } from "./basic_command_handlers.mjs";
15
15
  import { createCliCommandRegistry } from "./cli_commands.mjs";
16
16
  import { createCliSessionState, resetCommandSessionState, } from "./command_contract.mjs";
17
17
  import { createCommandEmitter } from "./command_output.mjs";
18
+ import { normalizeCliFailure } from "./failure_output.mjs";
18
19
  import { createGuidedHome } from "./guided.mjs";
19
20
  import { createProjectCommands } from "./project_commands.mjs";
20
21
  import { buildRuntimeStackPlan, createRuntimeCommands, runRuntimeStackDoctor, topologyCandidateSummary, topologyCommandEnvironment, } from "./runtime_commands.mjs";
@@ -217,7 +218,7 @@ try {
217
218
  await main();
218
219
  }
219
220
  catch (error) {
220
- const failure = normalizeRehearsalError(error, {
221
+ const failure = normalizeCliFailure(error, {
221
222
  expected: "a safe, versioned, local-only Rehearsal operation",
222
223
  actual: errorMessage(error),
223
224
  refused: "No further Rehearsal action was performed.",
@@ -168,13 +168,20 @@ export const createRuntimeActionHandlers = ({ prepareCandidateConfirmation, prep
168
168
  : undefined;
169
169
  const renderedHttpProofResults = "httpProofResults" in value ? value.httpProofResults : [];
170
170
  const renderedLifecycle = "lifecycle" in value ? value.lifecycle : undefined;
171
+ const hasApplicationLaunch = topology.primary.config.application.readiness !== null;
171
172
  const nextAction = {
172
173
  run: renderedLifecycle?.runtimeState === "stopped"
173
- ? "The runtime was stopped with its database and Storage changes preserved. Run rehearsal start or rehearsal open to resume."
174
- : "Next: run rehearsal open for hands-on testing, then rehearsal verify.",
175
- start: "Next: run rehearsal open for hands-on testing or rehearsal status.",
174
+ ? `The runtime was stopped with its database and Storage changes preserved. Run rehearsal start${hasApplicationLaunch ? " or rehearsal open" : ""} to resume.`
175
+ : hasApplicationLaunch
176
+ ? "Next: run rehearsal open for hands-on testing, then rehearsal verify."
177
+ : "The rehearsal command is complete; the database remains running. Next: run rehearsal verify or inspect the local database. This database-only project has no application to open.",
178
+ start: hasApplicationLaunch
179
+ ? "Next: run rehearsal open for hands-on testing or rehearsal status."
180
+ : "The database is running. Next: run rehearsal status or rehearsal verify; no application launch is configured.",
176
181
  migrate: "Next: run rehearsal verify to prove the current runtime.",
177
- reset: "Next: run rehearsal open or rehearsal verify.",
182
+ reset: hasApplicationLaunch
183
+ ? "Next: run rehearsal open or rehearsal verify."
184
+ : "Next: run rehearsal verify or inspect the local database; no application launch is configured.",
178
185
  status: "Next: run rehearsal verify, reset, stop, or discard.",
179
186
  stop: "Next: run rehearsal start when you want to resume.",
180
187
  discard: "The disposable runtime was removed; the immutable baseline remains.",
@@ -20,7 +20,8 @@ import { buildRuntimeManagerArguments } from "../targets/target.mjs";
20
20
  import { parseRuntimeStatusSnapshot, renderRuntimeStatusSnapshot, } from "../runtime/status.mjs";
21
21
  import { applyRuntimePersistence, shouldStopRuntime, } from "../runtime/persistence.mjs";
22
22
  import { executeRuntimeTargetStack } from "./runtime_stack_execution.mjs";
23
- import { summarizeRuntimeManagerFailure } from "./runtime_failure_summary.mjs";
23
+ import { summarizeRuntimeManagerFailure, runtimePortFailureGuidance, } from "./runtime_failure_summary.mjs";
24
+ import { readManagerMigrationFailure } from "../runtime/migration_failure.mjs";
24
25
  import { releaseOwnedColimaWhenIdle } from "../runtime/container_engine_ownership.mjs";
25
26
  import { inspectRehearsalResources } from "../runtime/resource_inventory.mjs";
26
27
  import { assertLocalDockerContext } from "../runtime/local_docker_context.mjs";
@@ -59,8 +60,12 @@ const parseCommand = (source) => {
59
60
  const projectCommandEnvironment = (additional = {}) => createCleanProcessEnvironment({ overrides: additional });
60
61
  const RUNTIME_STARTUP_FAILURE_PATTERN = /container\b.*\b(?:is not ready|starting|unhealthy)|health(?:check)?\b.*\b(?:failed|starting|unhealthy)|out of memory|\boom\b|no space left|resource temporarily unavailable/iu;
61
62
  export const classifyManagerFailure = ({ action, output, inferredCategory, }) => {
63
+ if (/^(?:Error:\s*)?The baseline (?:PostgreSQL|Supabase) migration failed \(/mu.test(output))
64
+ return "baseline_invalid";
65
+ if (/^(?:Error:\s*)?The confirmed candidate (?:PostgreSQL|Supabase) migration failed \(/mu.test(output))
66
+ return "migration_candidate_failure";
62
67
  if (RUNTIME_STARTUP_FAILURE_PATTERN.test(output) ||
63
- /Supabase startup failed \(/u.test(output) ||
68
+ /(?:PostgreSQL|Supabase) startup failed \(/u.test(output) ||
64
69
  /^Error:\s*supabase start(?:\s|$)/imu.test(output)) {
65
70
  return "runtime_dependency_failure";
66
71
  }
@@ -272,22 +277,31 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
272
277
  inferredCategory: inferred.category,
273
278
  });
274
279
  const startupFailure = category === "runtime_dependency_failure";
275
- const engineCapacity = startupFailure
280
+ const migrationFailure = readManagerMigrationFailure(completeOutput);
281
+ const portGuidance = runtimePortFailureGuidance(conciseOutput);
282
+ const engineCapacity = startupFailure && portGuidance.length === 0
276
283
  ? inspectContainerCapacity()
277
284
  : undefined;
278
285
  throw new RehearsalError({
279
286
  category,
280
287
  code: `MANAGER_${action.toUpperCase()}_FAILED`,
281
- message: `The local Rehearsal ${action} operation did not complete.`,
288
+ message: `The local Rehearsal ${action} operation did not complete.${migrationFailure ? ` ${migrationFailure.summary}` : ""}`,
282
289
  expected: "the isolated runtime operation to finish and verify",
283
290
  actual: conciseOutput || `exit ${result.status}`,
284
- context: { action, ...(engineCapacity ? { engineCapacity } : {}) },
291
+ context: {
292
+ action,
293
+ target: targetName ?? "primary",
294
+ ...(migrationFailure ? { migrationFailure } : {}),
295
+ ...(engineCapacity ? { engineCapacity } : {}),
296
+ },
285
297
  refused: "The runtime was not reported as trusted.",
286
298
  suggestions: [
299
+ ...(migrationFailure?.suggestions ?? []),
300
+ ...portGuidance,
287
301
  "Review the concise failure above, then rerun rehearsal doctor before retrying.",
288
- ...(startupFailure
302
+ ...(startupFailure && portGuidance.length === 0
289
303
  ? [
290
- "Stop unrelated local stacks or increase Docker/Colima capacity, then retry. Rehearsal will not change engine settings or bypass service health checks.",
304
+ "If startup evidence confirms a resource shortage, stop only stacks you own or increase Docker/Colima capacity, then retry. Rehearsal will not change engine settings or bypass service health checks.",
291
305
  "Run docker stats --no-stream to identify active containers using the shared engine.",
292
306
  ]
293
307
  : []),
@@ -1 +1,2 @@
1
+ export declare const runtimePortFailureGuidance: (output: string) => readonly string[];
1
2
  export declare const summarizeRuntimeManagerFailure: (output: string) => string;
@@ -1,5 +1,17 @@
1
1
  /** Keep useful manager failures rather than debug advice and trailing Node stacks. */
2
2
  import { redactDiagnosticValue } from "../shared/diagnostics.mjs";
3
+ import { readManagerMigrationFailure, renderMigrationFailureDiagnostic, } from "../runtime/migration_failure.mjs";
4
+ import { portStartupGuidance } from "../targets/port_start_failure.mjs";
5
+ export const runtimePortFailureGuidance = (output) => {
6
+ const provider = output.match(/^(?:Error:\s*)?(PostgreSQL|Supabase) startup failed \((?:exit -?\d+|process failure)\)\.$/mu)?.[1];
7
+ const evidence = output
8
+ .match(/^Reported startup evidence: ([a-z_, ]+)\.$/mu)?.[1]
9
+ ?.split(/,\s*/u);
10
+ const port = evidence?.find((value) => value === "port_in_use" || value === "port_unavailable");
11
+ return provider && (port === "port_in_use" || port === "port_unavailable")
12
+ ? portStartupGuidance(port, provider === "PostgreSQL" ? "postgresql" : "supabase").split("\n")
13
+ : [];
14
+ };
3
15
  export const summarizeRuntimeManagerFailure = (output) => {
4
16
  let nodeSourceContext = false;
5
17
  const lines = output
@@ -27,16 +39,22 @@ export const summarizeRuntimeManagerFailure = (output) => {
27
39
  .replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s"']+/giu, "[REDACTED_URL]")
28
40
  .replace(/\b[A-Z][A-Z0-9_]*\s*=\s*[^\s]+/gu, "[REDACTED_ENV]")
29
41
  .replace(/(?:\/[A-Za-z0-9_.-]+){2,}(?::\d+(?::\d+)?)?/gu, "[LOCAL_PATH]"));
30
- const startup = lines.findIndex((line) => /Supabase startup failed \(/u.test(line));
42
+ const startup = lines.findIndex((line) => /(?:PostgreSQL|Supabase) startup failed \(/u.test(line));
31
43
  const failure = lines.reduce((last, line, index) => (/^[A-Za-z]*Error:/u.test(line) ? index : last), -1);
32
44
  const migration = lines.findIndex((line) => /^Error: The (?:confirmed candidate|baseline) (?:PostgreSQL|Supabase) migration failed \(exit (?:-?\d+|unknown); stderr \d+ bytes\)\. SQL and provider output are withheld\.$/u.test(line));
33
45
  const rollbackFailed = /The runtime operation failed and its owned runtime could not be removed automatically\./u.test(output);
46
+ const migrationDiagnostic = migration >= 0 ? readManagerMigrationFailure(lines.join("\n")) : undefined;
34
47
  const selected = startup >= 0
35
48
  ? lines
36
- .slice(startup, startup + 4)
37
- .filter((line) => /^(?:Error:\s*)?Supabase startup failed \(|^Reported startup evidence:|^Reported service states:|^Provider logs, paths, environment values and connection URLs are omitted\./u.test(line))
49
+ .slice(startup, startup + 12)
50
+ .filter((line) => /^(?:Error:\s*)?(?:PostgreSQL|Supabase) startup failed \(|^Reported startup evidence:|^Reported service states:|^Provider logs, (?:command arguments, )?paths, environment values and connection URLs are omitted\./u.test(line))
38
51
  : migration >= 0
39
- ? [lines[migration]]
52
+ ? [
53
+ lines[migration],
54
+ ...(migrationDiagnostic
55
+ ? [renderMigrationFailureDiagnostic(migrationDiagnostic)]
56
+ : []),
57
+ ]
40
58
  : rollbackFailed
41
59
  ? [
42
60
  "Error: The operation failed; the original cause could not be safely summarized.",
@@ -44,10 +62,18 @@ export const summarizeRuntimeManagerFailure = (output) => {
44
62
  : failure >= 0
45
63
  ? lines.slice(failure, failure + 8)
46
64
  : lines.slice(-8);
65
+ if (startup >= 0)
66
+ selected.push(...runtimePortFailureGuidance(selected.join("\n")));
47
67
  if (rollbackFailed)
48
68
  selected.push("Owned rollback cleanup could not be confirmed. Resources may remain; inspect activity and status before retrying. No cleanup success is claimed.");
49
69
  if (startup >= 0 &&
50
70
  /owned cleanup could not be proved|owned resources were retained|Resources and data were retained|retained resources need review/iu.test(output))
51
71
  selected.push("Owned startup cleanup could not be proved; resources and data were retained. Inspect activity and status before retrying.");
72
+ if (selected.join("\n").length > 2_000 && migrationDiagnostic?.source) {
73
+ // Human summary stays bounded and retains cleanup warnings; structured output
74
+ // independently reads the complete, bounded manager frame.
75
+ const { source: _source, ...diagnostic } = migrationDiagnostic;
76
+ selected[1] = renderMigrationFailureDiagnostic(diagnostic);
77
+ }
52
78
  return selected.join("\n").slice(0, 2_000);
53
79
  };
@@ -126,6 +126,7 @@ export interface RehearsalConfig {
126
126
  protected?: boolean;
127
127
  target?: "supabase" | "postgresql";
128
128
  applicationUrl?: string;
129
+ /** Exact lowercase/digit/hyphen ownership ID: 2–40 Supabase, 2–63 PostgreSQL. */
129
130
  projectId?: string;
130
131
  apiPort?: number;
131
132
  databasePort: number;
@@ -9,6 +9,7 @@ import { normalizeResourcePolicy, normalizeRequiredServices, } from "./configura
9
9
  import { normalizeConfigurationProfiles } from "./configuration_profiles.mjs";
10
10
  import { normalizeApplicationUrls } from "./configuration_application_urls.mjs";
11
11
  import { normalizeApplicationActivity } from "./configuration_activity.mjs";
12
+ import { assertRuntimeProjectId } from "./runtime_project_identity.mjs";
12
13
  import { assertBoolean, assertCommand, assertDatabaseIdentifier, assertEnvironmentKey, assertEnvironmentReference, assertHttpStatus, assertIdentifier, assertKnownKeys, assertLoopbackUrl, assertNonEmptyString, assertPlainObject, assertPort, assertPostgresImage, assertPositiveInteger, assertRelativePath, assertStringArray, } from "./configuration_validation.mjs";
13
14
  const LOOPBACK_HOSTS = new Set(REHEARSAL_DEFAULTS.safety.allowedHosts);
14
15
  const SUPPORTED_SUPABASE_OAUTH_PROVIDERS = new Set(["github", "google"]);
@@ -351,7 +352,7 @@ export const normalizeRehearsalConfig = (input) => {
351
352
  protected: assertBoolean(runtime.protected ?? false, "config.runtime.protected"),
352
353
  target: runtimeTarget,
353
354
  applicationUrl: applicationUrls.applicationUrl,
354
- projectId: assertNonEmptyString(runtime.projectId ?? REHEARSAL_DEFAULTS.runtime.projectId, "config.runtime.projectId"),
355
+ projectId: assertRuntimeProjectId(runtime.projectId ?? REHEARSAL_DEFAULTS.runtime.projectId, runtimeTarget),
355
356
  ports: Object.freeze(ports),
356
357
  }),
357
358
  safety: Object.freeze({
@@ -1,4 +1,5 @@
1
1
  /** Render reviewed starter configurations without reading or writing a project. */
2
+ import { createRuntimeProjectId } from "./runtime_project_identity.mjs";
2
3
  export const renderDetectedConfig = (detected, { applicationUrl = "http://localhost:5175", ports = { api: 58321, database: 58322, studio: 58323 }, } = {}) => `// @ts-check
3
4
  /**
4
5
  * Rehearsal configuration for ${detected.projectName}.
@@ -132,7 +133,7 @@ export default defineRehearsalConfig({
132
133
  protected: false, // Pin data against reset, run, migrate, discard, and runtime cleanup.
133
134
  target: "supabase",
134
135
  applicationUrl: ${JSON.stringify(applicationUrl)},
135
- projectId: "${detected.projectName}-rehearsal",
136
+ projectId: ${JSON.stringify(createRuntimeProjectId(detected.projectName, "supabase", detected.projectIdSeed))},
136
137
  apiPort: ${ports.api},
137
138
  databasePort: ${ports.database},
138
139
  studioPort: ${ports.studio},
@@ -266,7 +267,7 @@ export default defineRehearsalConfig({
266
267
  protected: false, // Pin data against reset, run, migrate, discard, and runtime cleanup.
267
268
  target: "postgresql",
268
269
  applicationUrl: ${JSON.stringify(applicationUrl)},
269
- projectId: "${detected.projectName}-rehearsal",
270
+ projectId: ${JSON.stringify(createRuntimeProjectId(detected.projectName, "postgresql", detected.projectIdSeed))},
270
271
  databasePort: ${databasePort},
271
272
  },
272
273
 
@@ -3,7 +3,6 @@ import { basename, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { findRehearsalConfigPath, } from "./configuration_discovery.mjs";
5
5
  import { normalizeRehearsalConfig } from "./configuration_normalization.mjs";
6
- import { assertIdentifier } from "./configuration_validation.mjs";
7
6
  export const loadRehearsalConfig = async ({ projectRoot = process.cwd(), configPath, } = {}) => {
8
7
  const root = resolve(projectRoot);
9
8
  const path = await findRehearsalConfigPath({
@@ -74,7 +73,6 @@ export const loadRehearsalConfig = async ({ projectRoot = process.cwd(), configP
74
73
  throw new Error("config.preparation.privacyKey must resolve to a secret file inside config.baseline.artifactDirectory.");
75
74
  }
76
75
  }
77
- assertIdentifier(config.runtime.projectId, "config.runtime.projectId");
78
76
  return Object.freeze({
79
77
  config,
80
78
  configPath: path,
@@ -1,6 +1,8 @@
1
1
  /** Inspect project-local files used to render a conservative starter config. */
2
2
  export interface DetectedProject {
3
3
  projectName: string;
4
+ /** Untruncated normalized name keeps shortened new runtime identities distinct. */
5
+ projectIdSeed?: string;
4
6
  packageManager: "npm" | "pnpm" | "yarn";
5
7
  hasSupabaseConfig: boolean;
6
8
  hasMigrations: boolean;
@@ -31,6 +31,7 @@ export const inspectDetectedProject = async ({ projectRoot = process.cwd(), } =
31
31
  "rehearsal-project";
32
32
  return {
33
33
  projectName,
34
+ projectIdSeed: normalizedProjectName || projectName,
34
35
  packageManager,
35
36
  hasSupabaseConfig: await hasPath("supabase/config.toml"),
36
37
  hasMigrations: await hasPath("supabase/migrations"),
@@ -0,0 +1,5 @@
1
+ /** Preserve exact configured runtime ownership and bound newly generated identities. */
2
+ import type { RehearsalRuntimeTargetId } from "../targets/target.mjs";
3
+ export declare const assertRuntimeProjectId: (value: unknown, target: RehearsalRuntimeTargetId) => string;
4
+ /** Only new setup identities are shortened; valid existing generated IDs stay stable. */
5
+ export declare const createRuntimeProjectId: (projectName: string, target: RehearsalRuntimeTargetId, identitySeed?: string) => string;
@@ -0,0 +1,41 @@
1
+ /** Preserve exact configured runtime ownership and bound newly generated identities. */
2
+ import { createHash } from "node:crypto";
3
+ import { RehearsalError } from "../shared/diagnostics.mjs";
4
+ import { assertIdentifier, assertNonEmptyString, } from "./configuration_validation.mjs";
5
+ const maximumProjectIdLength = (target) => target === "supabase" ? 40 : 63;
6
+ export const assertRuntimeProjectId = (value, target) => {
7
+ const projectId = assertNonEmptyString(value, "config.runtime.projectId");
8
+ const maximumLength = maximumProjectIdLength(target);
9
+ if (projectId.length < 2 || projectId.length > maximumLength) {
10
+ const provider = target === "supabase" ? "Supabase" : "PostgreSQL";
11
+ throw new RehearsalError({
12
+ category: "configuration_invalid",
13
+ code: "CONFIGURATION_INVALID",
14
+ message: `config.runtime.projectId must contain 2–${maximumLength} characters for ${provider}.`,
15
+ expected: `2–${maximumLength} lowercase letters, digits, or hyphens`,
16
+ actual: { length: projectId.length },
17
+ context: { field: "runtime.projectId", target, maximumLength },
18
+ refused: "No runtime was started or renamed. Rehearsal never truncates a configured project ID or adopts resources by prefix.",
19
+ suggestions: [
20
+ "Choose a distinct, explicitly reviewed ID for a new disposable runtime. Review existing resources before changing their configured identity.",
21
+ ],
22
+ });
23
+ }
24
+ return assertIdentifier(projectId, "config.runtime.projectId");
25
+ };
26
+ /** Only new setup identities are shortened; valid existing generated IDs stay stable. */
27
+ export const createRuntimeProjectId = (projectName, target, identitySeed = projectName) => {
28
+ const candidate = `${projectName}-rehearsal`;
29
+ const maximumLength = maximumProjectIdLength(target);
30
+ if (candidate.length <= maximumLength)
31
+ return assertRuntimeProjectId(candidate, target);
32
+ const digest = createHash("sha256")
33
+ .update(`${identitySeed}-rehearsal`)
34
+ .digest("hex")
35
+ .slice(0, 12);
36
+ const suffix = `-${digest}-rehearsal`;
37
+ const prefix = projectName
38
+ .slice(0, maximumLength - suffix.length)
39
+ .replace(/-+$/u, "");
40
+ return assertRuntimeProjectId(`${prefix}${suffix}`, target);
41
+ };
@@ -8,6 +8,7 @@ import { access, mkdir, open, readFile, rename, rm, writeFile, } from "node:fs/p
8
8
  import { basename, dirname, join, relative, resolve } from "node:path";
9
9
  import { findRehearsalConfigPath, inspectDetectedProject, loadRehearsalConfig, renderDetectedConfig, renderDetectedPostgresqlConfig, } from "./configuration.mjs";
10
10
  import { resolveRuntimeTarget } from "../targets/target.mjs";
11
+ import { createRuntimeProjectId } from "./runtime_project_identity.mjs";
11
12
  export { assertSupportedRehearsalNodeRuntime, inspectRehearsalNodeRuntime, } from "../shared/node_runtime.mjs";
12
13
  import { assertSupportedRehearsalNodeRuntime } from "../shared/node_runtime.mjs";
13
14
  const CONFIG_PATH = "rehearsal.config.mjs";
@@ -257,7 +258,8 @@ export const planRehearsalSetup = async ({ projectRoot = process.cwd(), configPa
257
258
  ...(isPortAvailable ? { isAvailable: isPortAvailable } : {}),
258
259
  });
259
260
  const applicationUrl = existing?.config.runtime.applicationUrl ?? "http://localhost:5175";
260
- const projectId = existing?.config.runtime.projectId ?? `${detected.projectName}-rehearsal`;
261
+ const projectId = existing?.config.runtime.projectId ??
262
+ createRuntimeProjectId(detected.projectName, runtimeTarget, detected.projectIdSeed);
261
263
  const sourceSupabaseConfig = runtimeTarget === "supabase"
262
264
  ? await readOptionalFile(join(root, "supabase/config.toml"))
263
265
  : null;
@@ -0,0 +1,28 @@
1
+ /** Translate provider-native SQLSTATE evidence into fixed, privacy-safe guidance. */
2
+ import { type MigrationFailureSource } from "./migration_failure_source.mjs";
3
+ export type MigrationFailureClassification = "syntax" | "schema" | "constraint" | "permission" | "connectivity" | "timeout_or_cancel" | "unknown";
4
+ export interface MigrationFailureDiagnostic {
5
+ readonly schemaVersion: 1;
6
+ readonly provider: "postgresql" | "supabase";
7
+ readonly classification: MigrationFailureClassification;
8
+ readonly sqlState: string | null;
9
+ readonly evidence: "provider_sqlstate" | "unavailable" | "ambiguous";
10
+ readonly summary: string;
11
+ readonly suggestions: readonly string[];
12
+ readonly source?: MigrationFailureSource;
13
+ }
14
+ export declare const migrationFailureDiagnostic: ({ provider, sqlState, evidence, }: {
15
+ provider: MigrationFailureDiagnostic["provider"];
16
+ sqlState?: string | null;
17
+ evidence?: MigrationFailureDiagnostic["evidence"];
18
+ }) => MigrationFailureDiagnostic;
19
+ /** Read only known provider error envelopes, never arbitrary mentions of SQLSTATE. */
20
+ export declare const diagnoseMigrationFailure: ({ provider, stderr, }: {
21
+ provider: MigrationFailureDiagnostic["provider"];
22
+ stderr: string;
23
+ }) => MigrationFailureDiagnostic;
24
+ export declare const renderMigrationFailureDiagnostic: (diagnostic: MigrationFailureDiagnostic) => string;
25
+ /** Closed public projection: reject free text or metadata not generated by this owner. */
26
+ export declare const parseMigrationFailureDiagnostic: (value: unknown) => MigrationFailureDiagnostic;
27
+ /** Parse only Rehearsal's fixed migration header and adjacent fixed code line. */
28
+ export declare const readManagerMigrationFailure: (output: string) => MigrationFailureDiagnostic | undefined;