@rehearsal-db/core 1.0.1 → 1.0.2

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 (34) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +1 -1
  3. package/dist/package.json +1 -1
  4. package/dist/src/cli/failure_output.d.mts +3 -0
  5. package/dist/src/cli/failure_output.mjs +48 -0
  6. package/dist/src/cli/rehearsal.mjs +3 -2
  7. package/dist/src/cli/runtime_action_handlers.mjs +11 -4
  8. package/dist/src/cli/runtime_commands.mjs +21 -7
  9. package/dist/src/cli/runtime_failure_summary.d.mts +1 -0
  10. package/dist/src/cli/runtime_failure_summary.mjs +30 -4
  11. package/dist/src/runtime/migration_failure.d.mts +28 -0
  12. package/dist/src/runtime/migration_failure.mjs +197 -0
  13. package/dist/src/runtime/migration_failure_source.d.mts +16 -0
  14. package/dist/src/runtime/migration_failure_source.mjs +68 -0
  15. package/dist/src/shared/diagnostics.mjs +5 -0
  16. package/dist/src/shared/operation_guard.d.mts +4 -1
  17. package/dist/src/shared/operation_guard.mjs +18 -13
  18. package/dist/src/shared/operation_progress.mjs +35 -1
  19. package/dist/src/shared/operation_progress_storage.d.mts +27 -6
  20. package/dist/src/shared/operation_progress_storage.mjs +65 -9
  21. package/dist/src/targets/port_start_failure.d.mts +15 -0
  22. package/dist/src/targets/port_start_failure.mjs +46 -0
  23. package/dist/src/targets/postgresql.mjs +8 -7
  24. package/dist/src/targets/postgresql_process.d.mts +8 -1
  25. package/dist/src/targets/postgresql_process.mjs +46 -2
  26. package/dist/src/targets/supabase_process.mjs +2 -1
  27. package/dist/src/targets/supabase_start_failure.mjs +5 -1
  28. package/docs/architecture.md +32 -2
  29. package/docs/commands.md +35 -0
  30. package/docs/getting-started.md +1 -1
  31. package/docs/troubleshooting.md +54 -2
  32. package/docs/tutorial.md +93 -8
  33. package/docs/vscode.md +41 -16
  34. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,37 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.0.2] - 2026-10-20
9
+
10
+ ### Fixed
11
+
12
+ - Explain recognized PostgreSQL/Supabase migration failures with bounded native
13
+ SQLSTATE evidence and safe syntax, schema, constraint, permission, connectivity
14
+ and timeout guidance. Unsupported or ambiguous evidence remains unknown; raw
15
+ SQL, provider details, credentials and private rows are not exposed.
16
+ - Preserve the original diagnosis across cleanup and bounded operation history so
17
+ the CLI and editor can show the same failure after refresh. Failed operations
18
+ never claim verification or unconditional rollback success.
19
+ - Optionally identify the exact approved PostgreSQL input file by hash, without
20
+ inventing a failing SQL origin, line or column. Reject invalid UTF-8 before
21
+ execution rather than submitting replacement-decoded bytes.
22
+ - Point recognized bind failures at the configured local ports, including safe
23
+ Windows/WSL exclusion-inspection guidance. Do not infer a reservation from every
24
+ denial or suggest terminating unknown listeners.
25
+ - Keep database-only command guidance separate from optional application launch.
26
+
27
+ ### Documentation and distribution
28
+
29
+ - Add a main-tutorial existing-row constraint failure, reviewed backfill/fix and
30
+ verification sequence, plus a same-project VS Code handoff and safe local-copy
31
+ recovery for unsupported external npm links.
32
+ - Pair with Starter 0.1.22 for actionable persistent failures, safe exact-byte
33
+ source opening, distinct command/database states and truthful recorded proof.
34
+ - Refresh the separate macOS, Linux and Windows/WSL 2 setup bundles to exact core
35
+ 1.0.2. Native Windows database execution remains unsupported.
36
+
37
+ ## [1.0.1] - 2026-10-19
38
+
8
39
  ### Platform compatibility
9
40
 
10
41
  - Keep macOS Intel/Apple Silicon and Linux execution distinct from Windows desktop
@@ -784,7 +815,9 @@ not unattended cleanup, and container age is not proof of user inactivity.
784
815
  publication uses short-lived trusted OIDC, and every release tag must already exist on
785
816
  protected `main`.
786
817
 
787
- [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.0...HEAD
818
+ [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.2...HEAD
819
+ [1.0.2]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.1...v1.0.2
820
+ [1.0.1]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.0...v1.0.1
788
821
  [1.0.0]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.10...v1.0.0
789
822
  [0.1.0-rc.10]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.9...v0.1.0-rc.10
790
823
  [0.1.0-rc.9]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.8...v0.1.0-rc.9
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.0.2**;
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.
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.0.2",
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
  };
@@ -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;
@@ -0,0 +1,197 @@
1
+ /** Translate provider-native SQLSTATE evidence into fixed, privacy-safe guidance. */
2
+ import { parseMigrationFailureSource, readMigrationSourceFrame, renderMigrationSourceFrame, } from "./migration_failure_source.mjs";
3
+ const explanations = {
4
+ "42601": [
5
+ "syntax",
6
+ "The database reported a SQL syntax error.",
7
+ "Review the candidate SQL for missing keywords, punctuation or malformed statements; then review the new exact plan before retrying.",
8
+ ],
9
+ "23502": [
10
+ "constraint",
11
+ "Rows do not satisfy a NOT NULL requirement.",
12
+ "Review existing NULL values and any new writes. Backfill or transform incompatible rows before enforcing NOT NULL.",
13
+ ],
14
+ "23503": [
15
+ "constraint",
16
+ "Rows do not satisfy a foreign-key constraint.",
17
+ "Review missing referenced rows and migration ordering before enforcing the foreign key.",
18
+ ],
19
+ "23505": [
20
+ "constraint",
21
+ "Rows do not satisfy a uniqueness requirement.",
22
+ "Review duplicate values and choose a data-preserving resolution before adding the unique constraint or index.",
23
+ ],
24
+ "23514": [
25
+ "constraint",
26
+ "Rows do not satisfy a CHECK constraint.",
27
+ "Existing rows may not satisfy the new constraint. Review and backfill incompatible rows before enforcing it; also check values written by the migration.",
28
+ ],
29
+ "23P01": [
30
+ "constraint",
31
+ "Rows conflict with an exclusion constraint.",
32
+ "Review overlapping or conflicting values before enforcing the exclusion constraint.",
33
+ ],
34
+ "42P01": [
35
+ "schema",
36
+ "The migration references a table that is unavailable in this schema.",
37
+ "Review table names, schema qualification and migration ordering; this is not a confirmed syntax error.",
38
+ ],
39
+ "42703": [
40
+ "schema",
41
+ "The migration references a column that is unavailable in this schema.",
42
+ "Review column names and migration ordering; this is not a confirmed syntax error.",
43
+ ],
44
+ "42704": [
45
+ "schema",
46
+ "The migration references a database object that is unavailable.",
47
+ "Review object names, schema qualification and migration ordering.",
48
+ ],
49
+ "42804": [
50
+ "schema",
51
+ "The migration uses incompatible data types.",
52
+ "Review the column types and any required explicit, data-preserving conversion.",
53
+ ],
54
+ "42P07": [
55
+ "schema",
56
+ "The migration attempts to create a table that already exists.",
57
+ "Review baseline history and candidate ordering instead of deleting migration history.",
58
+ ],
59
+ "42701": [
60
+ "schema",
61
+ "The migration attempts to create a column that already exists.",
62
+ "Review baseline history and candidate ordering instead of deleting migration history.",
63
+ ],
64
+ "42501": [
65
+ "permission",
66
+ "The database denied permission for a migration operation.",
67
+ "Review the intended local role and required privileges; do not switch to hosted credentials or bypass permissions.",
68
+ ],
69
+ "28P01": [
70
+ "permission",
71
+ "The database rejected authentication.",
72
+ "Review the disposable local runtime credentials without printing or sharing them.",
73
+ ],
74
+ "57014": [
75
+ "timeout_or_cancel",
76
+ "The database reported a cancelled statement, possibly from a timeout.",
77
+ "Review statement timeout and cancellation evidence. No automatic retry or successful rollback is implied.",
78
+ ],
79
+ };
80
+ export const migrationFailureDiagnostic = ({ provider, sqlState = null, evidence = sqlState ? "provider_sqlstate" : "unavailable", }) => {
81
+ const safeCode = sqlState &&
82
+ /^[0-9A-Z]{5}$/u.test(sqlState) &&
83
+ !/^(?:00|01|02)/u.test(sqlState)
84
+ ? sqlState
85
+ : null;
86
+ const explanation = safeCode ? explanations[safeCode] : undefined;
87
+ const connectivity = safeCode?.startsWith("08");
88
+ return {
89
+ schemaVersion: 1,
90
+ provider,
91
+ classification: explanation?.[0] ?? (connectivity ? "connectivity" : "unknown"),
92
+ sqlState: safeCode,
93
+ evidence: safeCode
94
+ ? "provider_sqlstate"
95
+ : evidence === "ambiguous"
96
+ ? "ambiguous"
97
+ : "unavailable",
98
+ summary: explanation?.[1] ??
99
+ (connectivity
100
+ ? "The database reported a connection failure."
101
+ : "The failure could not be classified from recognized provider SQLSTATE evidence. It is not a confirmed syntax error."),
102
+ suggestions: [
103
+ explanation?.[2] ??
104
+ (connectivity
105
+ ? "Check the disposable local database connection and runtime status before retrying."
106
+ : "Review the exact candidate locally and its database requirements. Do not assume syntax, data or permissions are the cause."),
107
+ "No automatic SQL change or retry was performed. The failure itself does not prove rollback or cleanup; inspect runtime status before retrying.",
108
+ ],
109
+ };
110
+ };
111
+ /** Read only known provider error envelopes, never arbitrary mentions of SQLSTATE. */
112
+ export const diagnoseMigrationFailure = ({ provider, stderr, }) => {
113
+ // Truncation could hide a conflicting error. Refuse rather than guess from a slice.
114
+ if (Buffer.byteLength(stderr) > 128 * 1024)
115
+ return migrationFailureDiagnostic({ provider });
116
+ const codes = new Set();
117
+ for (const line of stderr
118
+ .replace(/\u001b\[[0-?]*[ -/]*[@-~]/gu, "")
119
+ .split(/\r?\n/u)) {
120
+ const match = provider === "postgresql"
121
+ ? line.match(/^(?:psql:(?:<stdin>|[^\r\n]+):\d+:\s*)?(?:ERROR|FATAL):\s+([0-9A-Z]{5})(?::(?:\s|$)|\s*$)/u)
122
+ : line.match(/^ERROR:\s+[^\r\n]*\(SQLSTATE ([0-9A-Z]{5})\)\s*$/u);
123
+ if (match?.[1] && !/^(?:00|01|02)/u.test(match[1]))
124
+ codes.add(match[1]);
125
+ }
126
+ return migrationFailureDiagnostic({
127
+ provider,
128
+ sqlState: codes.size === 1 ? [...codes][0] : null,
129
+ evidence: codes.size > 1 ? "ambiguous" : "unavailable",
130
+ });
131
+ };
132
+ export const renderMigrationFailureDiagnostic = (diagnostic) => [
133
+ `Migration SQLSTATE: ${diagnostic.sqlState ?? diagnostic.evidence}.`,
134
+ ...(diagnostic.source === undefined
135
+ ? []
136
+ : [renderMigrationSourceFrame(diagnostic.source)]),
137
+ diagnostic.summary,
138
+ ...diagnostic.suggestions,
139
+ ].join("\n");
140
+ /** Closed public projection: reject free text or metadata not generated by this owner. */
141
+ export const parseMigrationFailureDiagnostic = (value) => {
142
+ if (!value || typeof value !== "object" || Array.isArray(value))
143
+ throw new Error("Invalid safe migration diagnostic.");
144
+ const record = value;
145
+ if (Object.keys(record)
146
+ .filter((key) => key !== "source")
147
+ .sort()
148
+ .join(",") !==
149
+ "classification,evidence,provider,schemaVersion,sqlState,suggestions,summary" ||
150
+ record.schemaVersion !== 1 ||
151
+ typeof record.provider !== "string" ||
152
+ !["postgresql", "supabase"].includes(record.provider) ||
153
+ !(record.sqlState === null || typeof record.sqlState === "string") ||
154
+ !["provider_sqlstate", "unavailable", "ambiguous"].includes(String(record.evidence)))
155
+ throw new Error("Invalid safe migration diagnostic.");
156
+ const canonical = migrationFailureDiagnostic({
157
+ provider: record.provider,
158
+ sqlState: record.sqlState,
159
+ evidence: record.evidence,
160
+ });
161
+ if (record.classification !== canonical.classification ||
162
+ record.sqlState !== canonical.sqlState ||
163
+ record.evidence !== canonical.evidence ||
164
+ record.summary !== canonical.summary ||
165
+ JSON.stringify(record.suggestions) !== JSON.stringify(canonical.suggestions))
166
+ throw new Error("Invalid safe migration diagnostic.");
167
+ return record.source === undefined
168
+ ? canonical
169
+ : { ...canonical, source: parseMigrationFailureSource(record.source) };
170
+ };
171
+ /** Parse only Rehearsal's fixed migration header and adjacent fixed code line. */
172
+ export const readManagerMigrationFailure = (output) => {
173
+ if (Buffer.byteLength(output) > 128 * 1024)
174
+ return undefined;
175
+ const match = output.match(/^(?:[\t ]*Error:[\t ]*)?The (?:confirmed candidate|baseline) (PostgreSQL|Supabase) migration failed \(exit (?:-?\d+|unknown); stderr \d+ bytes\)\. SQL and provider output are withheld\.\r?\n[\t ]*Migration SQLSTATE: ([0-9A-Z]{5}|unavailable|ambiguous)\.[\t ]*$/mu);
176
+ if (!match)
177
+ return undefined;
178
+ const code = match[2];
179
+ const diagnostic = migrationFailureDiagnostic({
180
+ provider: match[1] === "PostgreSQL" ? "postgresql" : "supabase",
181
+ sqlState: /^[0-9A-Z]{5}$/u.test(code) ? code : null,
182
+ evidence: code === "ambiguous" ? "ambiguous" : "unavailable",
183
+ });
184
+ const afterCode = output
185
+ .slice(match.index + match[0].length)
186
+ .replace(/^\r?\n/u, "")
187
+ .split(/\r?\n/u)[0];
188
+ const source = readMigrationSourceFrame(output, afterCode);
189
+ const expectedKind = /The baseline /u.test(match[0])
190
+ ? "baseline"
191
+ : "candidate";
192
+ return source === undefined ||
193
+ source.kind !== expectedKind ||
194
+ diagnostic.provider !== "postgresql"
195
+ ? diagnostic
196
+ : { ...diagnostic, source };
197
+ };
@@ -0,0 +1,16 @@
1
+ /** Closed attempted-input provenance. No statement/source-position inference. */
2
+ export interface MigrationFailureSource {
3
+ readonly schemaVersion: 1;
4
+ readonly kind: "baseline" | "candidate";
5
+ readonly filename: string;
6
+ readonly sha256: string;
7
+ readonly workspaceRelativePath: string | null;
8
+ readonly line: null;
9
+ readonly column: null;
10
+ readonly evidence: "owned_execution_input";
11
+ }
12
+ export declare const isMigrationWorkspacePath: (value: unknown) => value is string;
13
+ export declare const parseMigrationFailureSource: (value: unknown) => MigrationFailureSource;
14
+ export declare const renderMigrationSourceFrame: (source: MigrationFailureSource) => string;
15
+ /** Only a single complete, adjacent core frame can contribute provenance. */
16
+ export declare const readMigrationSourceFrame: (output: string, adjacentLine: string | undefined) => MigrationFailureSource | undefined;
@@ -0,0 +1,68 @@
1
+ const controls = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u;
2
+ export const isMigrationWorkspacePath = (value) => typeof value === "string" &&
3
+ Buffer.byteLength(value) <= 1024 &&
4
+ value.length > 0 &&
5
+ !/[\\:%?#]/u.test(value) &&
6
+ !controls.test(value) &&
7
+ value
8
+ .split("/")
9
+ .every((part) => part.length > 0 && part !== "." && part !== "..");
10
+ export const parseMigrationFailureSource = (value) => {
11
+ if (!value || typeof value !== "object" || Array.isArray(value))
12
+ throw new Error("Invalid migration input provenance.");
13
+ const source = value;
14
+ if (Object.keys(source).sort().join(",") !==
15
+ "column,evidence,filename,kind,line,schemaVersion,sha256,workspaceRelativePath" ||
16
+ source.schemaVersion !== 1 ||
17
+ !["baseline", "candidate"].includes(String(source.kind)) ||
18
+ typeof source.kind !== "string" ||
19
+ typeof source.filename !== "string" ||
20
+ Buffer.byteLength(source.filename) > 255 ||
21
+ !/^\d{14}_[a-z0-9_]+\.sql$/u.test(source.filename) ||
22
+ typeof source.sha256 !== "string" ||
23
+ !/^[a-f0-9]{64}$/u.test(source.sha256) ||
24
+ (source.workspaceRelativePath !== null &&
25
+ (!isMigrationWorkspacePath(source.workspaceRelativePath) ||
26
+ source.workspaceRelativePath.split("/").at(-1) !== source.filename)) ||
27
+ source.line !== null ||
28
+ source.column !== null ||
29
+ source.evidence !== "owned_execution_input")
30
+ throw new Error("Invalid migration input provenance.");
31
+ return {
32
+ schemaVersion: 1,
33
+ kind: source.kind,
34
+ filename: source.filename,
35
+ sha256: source.sha256,
36
+ workspaceRelativePath: source.workspaceRelativePath,
37
+ line: null,
38
+ column: null,
39
+ evidence: "owned_execution_input",
40
+ };
41
+ };
42
+ const framePrefix = "Rehearsal migration input v1: ";
43
+ export const renderMigrationSourceFrame = (source) => framePrefix +
44
+ Buffer.from(JSON.stringify(parseMigrationFailureSource(source))).toString("base64url");
45
+ /** Only a single complete, adjacent core frame can contribute provenance. */
46
+ export const readMigrationSourceFrame = (output, adjacentLine) => {
47
+ try {
48
+ const frames = output
49
+ .split(/\r?\n/u)
50
+ .map((line) => line.trim())
51
+ .filter((line) => line.startsWith(framePrefix));
52
+ if (frames.length !== 1 || adjacentLine?.trim() !== frames[0])
53
+ return undefined;
54
+ const encoded = frames[0].slice(framePrefix.length);
55
+ if (encoded.length > 4096 || !/^[A-Za-z0-9_-]+$/u.test(encoded))
56
+ return undefined;
57
+ const bytes = Buffer.from(encoded, "base64url");
58
+ if (bytes.length > 2048 || bytes.toString("base64url") !== encoded)
59
+ return undefined;
60
+ const source = parseMigrationFailureSource(JSON.parse(bytes.toString("utf8")));
61
+ return renderMigrationSourceFrame(source) === frames[0]
62
+ ? source
63
+ : undefined;
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ };
@@ -84,6 +84,11 @@ export class RehearsalError extends Error {
84
84
  const errorMessage = (error) => error instanceof Error ? error.message : String(error);
85
85
  const inferCategory = (error) => {
86
86
  const message = errorMessage(error);
87
+ // Fixed driver phase precedes incidental words in safe remediation text.
88
+ if (/^(?:Error:\s*)?The baseline (?:PostgreSQL|Supabase) migration failed \(/mu.test(message))
89
+ return "baseline_invalid";
90
+ if (/^(?:Error:\s*)?The confirmed candidate (?:PostgreSQL|Supabase) migration failed \(/mu.test(message))
91
+ return "migration_candidate_failure";
87
92
  if (/checksum/iu.test(message))
88
93
  return "baseline_checksum_mismatch";
89
94
  if (/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.test(message)) {