@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
@@ -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
+ };
@@ -1,4 +1,5 @@
1
1
  /** Build narrowly validated local restore prerequisites and structural checks. */
2
+ import { type RuntimePrivilege } from "./privileges.mjs";
2
3
  type PolicyObject = Record<string, unknown>;
3
4
  interface RuntimeRelation {
4
5
  readonly schema: string;
@@ -55,6 +56,7 @@ export interface RuntimePolicy {
55
56
  readonly triggers: readonly RuntimeTrigger[];
56
57
  readonly localRows: readonly RuntimeLocalRow[];
57
58
  readonly expectations: readonly RuntimeExpectation[];
59
+ readonly privileges?: readonly RuntimePrivilege[];
58
60
  }
59
61
  export declare const IDENTITY_CLAIM_RECEIPT_SQL = "create schema if not exists rehearsal_internal;\ncreate table if not exists rehearsal_internal.identity_claims (\n identity_name text primary key,\n placeholder_user_id uuid not null unique,\n local_user_id uuid not null,\n claimed_at timestamptz not null default now()\n);";
60
62
  export declare const validateRuntimePolicy: (input: unknown) => RuntimePolicy;
@@ -1,4 +1,5 @@
1
1
  /** Build narrowly validated local restore prerequisites and structural checks. */
2
+ import { buildRuntimePrivilegeVerificationChecks, validateRuntimePrivileges, } from "./privileges.mjs";
2
3
  const IDENTIFIER = /^[a-z][a-z0-9_]{0,62}$/u;
3
4
  const IDENTITY_NAME = /^[a-z0-9][a-z0-9-]{1,62}$/u;
4
5
  const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/iu;
@@ -58,7 +59,14 @@ create table if not exists rehearsal_internal.identity_claims (
58
59
  );`;
59
60
  export const validateRuntimePolicy = (input) => {
60
61
  const policy = object(input, "runtimePolicy");
61
- keys(policy, ["policyVersion", "prerequisites", "triggers", "localRows", "expectations"], "runtimePolicy");
62
+ keys(policy, [
63
+ "policyVersion",
64
+ "prerequisites",
65
+ "triggers",
66
+ "localRows",
67
+ "expectations",
68
+ "privileges",
69
+ ], "runtimePolicy");
62
70
  if (policy.policyVersion !== 1)
63
71
  throw new Error("Runtime policy must use policyVersion 1.");
64
72
  const prerequisites = object(policy.prerequisites ?? {}, "runtimePolicy.prerequisites");
@@ -216,6 +224,9 @@ export const validateRuntimePolicy = (input) => {
216
224
  triggers: Object.freeze(triggers),
217
225
  localRows: Object.freeze(localRows),
218
226
  expectations: Object.freeze(expectations),
227
+ ...(Object.hasOwn(policy, "privileges")
228
+ ? { privileges: validateRuntimePrivileges(policy.privileges) }
229
+ : {}),
219
230
  });
220
231
  };
221
232
  export const buildRuntimePrerequisiteSql = (input) => {
@@ -360,6 +371,7 @@ export const buildRuntimeVerificationSql = (input) => {
360
371
  }
361
372
  checks.push(`if exists (select 1 from pg_constraint where not convalidated) then
362
373
  raise exception 'Rehearsal found an unvalidated structural constraint'; end if;`);
374
+ checks.push(...buildRuntimePrivilegeVerificationChecks(policy.privileges ?? []));
363
375
  return `do $rehearsal_verify$
364
376
  begin
365
377
  ${checks.join("\n ")}
@@ -0,0 +1,25 @@
1
+ /** Validate opt-in effective privileges and build catalog-only verification checks. */
2
+ export interface RuntimePrivilegeArgumentType {
3
+ readonly schema: "pg_catalog";
4
+ readonly name: string;
5
+ }
6
+ interface RuntimePrivilegeBase {
7
+ readonly schema: string;
8
+ readonly role: string;
9
+ readonly required: readonly string[];
10
+ readonly forbidden: readonly string[];
11
+ }
12
+ export type RuntimePrivilege = RuntimePrivilegeBase & ({
13
+ readonly kind: "schema";
14
+ } | {
15
+ readonly kind: "table" | "sequence";
16
+ readonly name: string;
17
+ } | {
18
+ readonly kind: "function";
19
+ readonly name: string;
20
+ readonly argumentTypes: readonly RuntimePrivilegeArgumentType[];
21
+ });
22
+ export declare const validateRuntimePrivileges: (input: unknown) => readonly RuntimePrivilege[];
23
+ /** Blocks are embedded inside the policy verifier; no ACL or object writes are emitted. */
24
+ export declare const buildRuntimePrivilegeVerificationChecks: (entries: readonly RuntimePrivilege[]) => readonly string[];
25
+ export {};
@@ -0,0 +1,198 @@
1
+ /** Validate opt-in effective privileges and build catalog-only verification checks. */
2
+ const IDENTIFIER = /^[a-z][a-z0-9_]{0,62}$/u;
3
+ const TYPE_IDENTIFIER = /^[a-z_][a-z0-9_]{0,62}$/u;
4
+ const PRIVILEGES = {
5
+ schema: ["CREATE", "USAGE"],
6
+ table: [
7
+ "SELECT",
8
+ "INSERT",
9
+ "UPDATE",
10
+ "DELETE",
11
+ "TRUNCATE",
12
+ "REFERENCES",
13
+ "TRIGGER",
14
+ "MAINTAIN",
15
+ ],
16
+ sequence: ["USAGE", "SELECT", "UPDATE"],
17
+ function: ["EXECUTE"],
18
+ };
19
+ const COLUMN_PRIVILEGES = new Set(["SELECT", "INSERT", "UPDATE", "REFERENCES"]);
20
+ const object = (value, label) => {
21
+ if (value === null ||
22
+ typeof value !== "object" ||
23
+ Array.isArray(value) ||
24
+ Object.getPrototypeOf(value) !== Object.prototype) {
25
+ throw new Error(`${label} must be an object.`);
26
+ }
27
+ return value;
28
+ };
29
+ const keys = (value, allowed, label) => {
30
+ if (Object.keys(value).some((key) => !allowed.includes(key)))
31
+ throw new Error(`${label} contains unsupported fields.`);
32
+ };
33
+ const identifier = (value, label) => {
34
+ if (typeof value !== "string" || !IDENTIFIER.test(value))
35
+ throw new Error(`${label} is not a safe PostgreSQL identifier.`);
36
+ return value;
37
+ };
38
+ const permissions = (value, allowed, label) => {
39
+ if (!Array.isArray(value) ||
40
+ value.length > allowed.length ||
41
+ value.includes(undefined) ||
42
+ value.some((entry) => typeof entry !== "string" || !allowed.includes(entry)) ||
43
+ new Set(value).size !== value.length) {
44
+ throw new Error(`${label} must contain distinct supported privileges.`);
45
+ }
46
+ return Object.freeze([...value]);
47
+ };
48
+ export const validateRuntimePrivileges = (input) => {
49
+ if (!Array.isArray(input) ||
50
+ input.length > 1_024 ||
51
+ input.includes(undefined))
52
+ throw new Error("runtimePolicy.privileges must be an array of at most 1024 declarations.");
53
+ const observed = new Map();
54
+ return Object.freeze(input.map((value, index) => {
55
+ const label = `runtimePolicy.privileges[${index}]`;
56
+ const entry = object(value, label);
57
+ if (typeof entry.kind !== "string" ||
58
+ !Object.hasOwn(PRIVILEGES, entry.kind)) {
59
+ throw new Error(`${label}.kind is unsupported.`);
60
+ }
61
+ const kind = entry.kind;
62
+ keys(entry, [
63
+ "kind",
64
+ "schema",
65
+ "role",
66
+ "required",
67
+ "forbidden",
68
+ ...(kind === "schema" ? [] : ["name"]),
69
+ ...(kind === "function" ? ["argumentTypes"] : []),
70
+ ], label);
71
+ const schema = identifier(entry.schema, `${label}.schema`);
72
+ const role = identifier(entry.role, `${label}.role`);
73
+ const allowed = PRIVILEGES[kind];
74
+ const required = permissions(Object.hasOwn(entry, "required") ? entry.required : [], allowed, `${label}.required`);
75
+ const forbidden = permissions(Object.hasOwn(entry, "forbidden") ? entry.forbidden : [], allowed, `${label}.forbidden`);
76
+ if (required.length + forbidden.length === 0)
77
+ throw new Error(`${label} must declare a required or forbidden privilege.`);
78
+ const base = { schema, role, required, forbidden };
79
+ let normalized;
80
+ if (kind === "schema")
81
+ normalized = Object.freeze({ ...base, kind });
82
+ else {
83
+ const name = identifier(entry.name, `${label}.name`);
84
+ if (kind !== "function")
85
+ normalized = Object.freeze({ ...base, kind, name });
86
+ else {
87
+ if (!Array.isArray(entry.argumentTypes) ||
88
+ entry.argumentTypes.length > 32 ||
89
+ entry.argumentTypes.includes(undefined))
90
+ throw new Error(`${label}.argumentTypes must be an array of at most 32 types.`);
91
+ const argumentTypes = entry.argumentTypes.map((value, argumentIndex) => {
92
+ const typeLabel = `${label}.argumentTypes[${argumentIndex}]`;
93
+ const type = object(value, typeLabel);
94
+ keys(type, ["schema", "name"], typeLabel);
95
+ if (type.schema !== "pg_catalog" ||
96
+ typeof type.name !== "string" ||
97
+ !TYPE_IDENTIFIER.test(type.name)) {
98
+ throw new Error(`${typeLabel} must name a safe pg_catalog type.`);
99
+ }
100
+ return Object.freeze({
101
+ schema: "pg_catalog",
102
+ name: type.name,
103
+ });
104
+ });
105
+ normalized = Object.freeze({
106
+ ...base,
107
+ kind,
108
+ name,
109
+ argumentTypes: Object.freeze(argumentTypes),
110
+ });
111
+ }
112
+ }
113
+ const identity = JSON.stringify([
114
+ kind,
115
+ schema,
116
+ normalized.kind === "schema" ? null : normalized.name,
117
+ role,
118
+ normalized.kind === "function" ? normalized.argumentTypes : null,
119
+ ]);
120
+ const earlier = observed.get(identity) ?? {
121
+ required: new Set(),
122
+ forbidden: new Set(),
123
+ };
124
+ if (required.some((privilege) => forbidden.includes(privilege) || earlier.forbidden.has(privilege)) ||
125
+ forbidden.some((privilege) => earlier.required.has(privilege))) {
126
+ throw new Error(`${label} has overlapping required and forbidden privileges.`);
127
+ }
128
+ required.forEach((privilege) => earlier.required.add(privilege));
129
+ forbidden.forEach((privilege) => earlier.forbidden.add(privilege));
130
+ observed.set(identity, earlier);
131
+ return normalized;
132
+ }));
133
+ };
134
+ const literal = (value) => `'${value.replaceAll("'", "''")}'`;
135
+ const typeLookup = (type) => `select t.oid from pg_catalog.pg_type t
136
+ join pg_catalog.pg_namespace n on n.oid = t.typnamespace
137
+ where n.nspname = 'pg_catalog' and t.typname = ${literal(type.name)}`;
138
+ const objectLookup = (entry) => {
139
+ if (entry.kind === "schema")
140
+ return `select n.oid into rehearsal_object from pg_catalog.pg_namespace n
141
+ where n.nspname = ${literal(entry.schema)};`;
142
+ if (entry.kind === "function")
143
+ return `select p.oid into rehearsal_object from pg_catalog.pg_proc p
144
+ join pg_catalog.pg_namespace n on n.oid = p.pronamespace
145
+ where n.nspname = ${literal(entry.schema)} and p.proname = ${literal(entry.name)}
146
+ and p.prokind = 'f' and p.pronargs = ${entry.argumentTypes.length}
147
+ ${entry.argumentTypes.map((type, index) => `and p.proargtypes[${index}] = (${typeLookup(type)})`).join("\n ")};`;
148
+ return `select c.oid into rehearsal_object from pg_catalog.pg_class c
149
+ join pg_catalog.pg_namespace n on n.oid = c.relnamespace
150
+ where n.nspname = ${literal(entry.schema)} and c.relname = ${literal(entry.name)}
151
+ and c.relkind ${entry.kind === "sequence" ? "= 'S'" : "in ('r', 'p', 'v', 'm', 'f')"};`;
152
+ };
153
+ const permissionCheck = (entry, privilege, forbidden) => {
154
+ const functionName = `pg_catalog.has_${entry.kind}_privilege`;
155
+ const tableCheck = `${functionName}(rehearsal_role, rehearsal_object, ${literal(privilege)})`;
156
+ const expression = entry.kind === "table" && forbidden && COLUMN_PRIVILEGES.has(privilege)
157
+ ? `(${tableCheck} or pg_catalog.has_any_column_privilege(rehearsal_role, rehearsal_object, ${literal(privilege)}))`
158
+ : tableCheck;
159
+ const evaluation = privilege === "MAINTAIN"
160
+ ? `if pg_catalog.current_setting('server_version_num')::integer < 170000 then
161
+ rehearsal_allowed := false;
162
+ else
163
+ rehearsal_allowed := ${expression};
164
+ end if;`
165
+ : `rehearsal_allowed := ${expression};`;
166
+ return `${evaluation}
167
+ if rehearsal_allowed is null then
168
+ raise exception 'Rehearsal privilege object is unavailable';
169
+ end if;
170
+ if rehearsal_allowed is ${forbidden ? "true" : "false"} then
171
+ raise exception 'Rehearsal ${forbidden ? "forbidden privilege is present" : "required privilege is missing"}';
172
+ end if;`;
173
+ };
174
+ /** Blocks are embedded inside the policy verifier; no ACL or object writes are emitted. */
175
+ export const buildRuntimePrivilegeVerificationChecks = (entries) => entries.map((entry) => `declare
176
+ rehearsal_role pg_catalog.oid;
177
+ rehearsal_object pg_catalog.oid;
178
+ rehearsal_allowed boolean;
179
+ begin
180
+ select r.oid into rehearsal_role from pg_catalog.pg_roles r
181
+ where r.rolname = ${literal(entry.role)};
182
+ if rehearsal_role is null then
183
+ raise exception 'Rehearsal privilege role is missing';
184
+ end if;
185
+ ${entry.kind === "function"
186
+ ? entry.argumentTypes
187
+ .map((type) => `if not exists (${typeLookup(type)}) then
188
+ raise exception 'Rehearsal privilege argument type is missing';
189
+ end if;`)
190
+ .join("\n ")
191
+ : ""}
192
+ ${objectLookup(entry)}
193
+ if rehearsal_object is null then
194
+ raise exception 'Rehearsal privilege object is missing';
195
+ end if;
196
+ ${entry.required.map((privilege) => permissionCheck(entry, privilege, false)).join("\n ")}
197
+ ${entry.forbidden.map((privilege) => permissionCheck(entry, privilege, true)).join("\n ")}
198
+ end;`);
@@ -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)) {
@@ -1,5 +1,7 @@
1
1
  /** Project-wide mutation lock and installed-package integrity guard. */
2
+ import type { MigrationFailureDiagnostic } from "../runtime/migration_failure.mjs";
2
3
  import { type OperationProgressSnapshot, type RecentOperationProgress } from "./operation_progress_contract.mjs";
4
+ import { type RecentOperationFailure } from "./operation_progress_storage.mjs";
3
5
  export interface InstalledPackageFingerprint {
4
6
  readonly files: number;
5
7
  readonly sha256: string;
@@ -8,7 +10,7 @@ export interface ProjectOperation {
8
10
  readonly command: string;
9
11
  readonly id: string;
10
12
  readonly startedAt: string;
11
- writeProgress(progress: OperationProgressSnapshot): Promise<void>;
13
+ writeProgress(progress: OperationProgressSnapshot, migrationFailure?: MigrationFailureDiagnostic): Promise<void>;
12
14
  release(): Promise<void>;
13
15
  releaseSync(): void;
14
16
  }
@@ -22,6 +24,7 @@ export type ProjectOperationState = Readonly<{
22
24
  progress: OperationProgressSnapshot | null;
23
25
  }> | null;
24
26
  recent: readonly RecentOperationProgress[] | null;
27
+ recentFailures?: readonly RecentOperationFailure[] | null;
25
28
  }>;
26
29
  export declare const createInstalledPackageFingerprint: ({ packageRoot, }: {
27
30
  packageRoot: string;