@rehearsal-db/core 0.1.0-beta.2 → 0.1.0-beta.3

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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,40 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## [0.1.0-beta.3] - 2026-10-01
9
+
10
+ ### Added
11
+
12
+ - A state-aware interactive home screen that guides setup, baseline preparation,
13
+ migration review, runtime management, verification, and cleanup without requiring
14
+ users to memorize commands.
15
+ - A safe `setup` workflow that previews and creates a dedicated local Supabase config,
16
+ chooses an available port block, updates protective ignore rules, and summarizes
17
+ readiness without overwriting project files.
18
+ - A schema-only `baseline prepare` workflow that generates a fail-closed policy draft
19
+ with explicit `REVIEW REQUIRED` decisions and never prints source row values.
20
+ - Interactive confirmation of the exact candidate migration set plus friendly progress
21
+ and timing while a rehearsal runs.
22
+
23
+ ### Changed
24
+
25
+ - Human output now recommends the next useful action, uses compact first-run readiness
26
+ summaries, respects `NO_COLOR` and `--plain`, and renders singular counts correctly.
27
+ - Bare `rehearsal` opens the guide only in a terminal and remains noninteractive and
28
+ script-safe when standard input or output is redirected.
29
+
30
+ ### Fixed
31
+
32
+ - Setup detects unsupported Node.js versions before writing, ignores ports occupied by
33
+ Docker or SSH forwarding, and rechecks its selected ports immediately before commit.
34
+ - Generated project identifiers are bounded to values accepted by the local runtime.
35
+
36
+ ### Security
37
+
38
+ - Reviewed sanitization policy bytes are checksum-bound to the active baseline and are
39
+ revalidated during planning and runtime restore.
40
+ - Draft or incomplete sanitization policies cannot be activated as baselines.
41
+
8
42
  ## [0.1.0-beta.2] - 2026-09-15
9
43
 
10
44
  ### Fixed
@@ -72,7 +106,8 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
72
106
  publication uses short-lived trusted OIDC, and every release tag must already exist on
73
107
  protected `main`.
74
108
 
75
- [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.2...HEAD
109
+ [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.3...HEAD
110
+ [0.1.0-beta.3]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.2...v0.1.0-beta.3
76
111
  [0.1.0-beta.2]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.1...v0.1.0-beta.2
77
112
  [0.1.0-beta.1]: https://github.com/Ddupasquier/rehearsal-db/releases/tag/v0.1.0-beta.1
78
113
  [0.1.0-beta.0]: https://github.com/Ddupasquier/rehearsal-db/releases/tag/v0.1.0-beta.0
package/README.md CHANGED
@@ -84,8 +84,13 @@ Contributors testing an unreleased change can use `npm link` or install the tarb
84
84
  produced by `npm pack` from a local checkout. In a consuming project, the commands are:
85
85
 
86
86
  ```bash
87
+ npx rehearsal
88
+ npx rehearsal setup
89
+ npx rehearsal setup --write
87
90
  npx rehearsal init
88
91
  npx rehearsal init --write
92
+ npx rehearsal baseline prepare --records=<safe.ndjson> --ledger=<ledger.json>
93
+ npx rehearsal baseline prepare --records=<safe.ndjson> --ledger=<ledger.json> --write
89
94
  npx rehearsal baseline create --records=<safe.ndjson> --ledger=<ledger.json>
90
95
  npx rehearsal doctor
91
96
  npx rehearsal explain
@@ -102,14 +107,32 @@ npx rehearsal stop
102
107
  npx rehearsal discard
103
108
  ```
104
109
 
110
+ Running `npx rehearsal` in a terminal opens a state-aware guide that shows completed
111
+ setup steps and recommends available actions. The explicit commands remain the stable
112
+ interface for automation and CI.
113
+
114
+ `setup` previews a conservative first-run scaffold: the Rehearsal configuration, a
115
+ dedicated local-only Supabase configuration on an available port block, and protective
116
+ `.gitignore` entries. It writes only with `--write`, never overwrites project files, and
117
+ does not copy enabled external providers from the application's Supabase configuration.
118
+ After writing, it runs the same readiness checks as `doctor` and shows the remaining
119
+ project-owned inputs. Use `init` when you want to create only the configuration file
120
+ manually.
121
+
122
+ `baseline prepare` inspects only the shape of explicit local synthetic records and writes
123
+ a fail-closed sanitization-policy draft. Every column remains `REVIEW REQUIRED` until a
124
+ human classifies its sanitization action, generated/identity behavior, and foreign key.
125
+ Drafts cannot be activated, and a reviewed policy is checksum-bound to its baseline.
126
+
105
127
  `init` previews a type-aware ESM `rehearsal.config.mjs`; it writes only with `--write`
106
128
  and never overwrites an existing file. The explicit `.mjs` extension makes the generated
107
129
  configuration executable in both CommonJS and ESM projects. Review all detected values.
108
130
  Rehearsal intentionally does not detect, copy, or enable a hosted project.
109
131
 
110
132
  `doctor` must end with `READY` before execution. `explain` and `run --dry-run` use the
111
- same immutable planner and perform no state-changing operations. If migrations are
112
- pending, execution requires the exact candidate digest printed by the plan:
133
+ same immutable planner and perform no state-changing operations. In a terminal,
134
+ `rehearsal run` displays and confirms the exact candidate set before touching the local
135
+ runtime. Automation supplies the digest explicitly:
113
136
 
114
137
  ```bash
115
138
  npx rehearsal run --confirm-candidates=<sha256>
package/docs/commands.md CHANGED
@@ -6,8 +6,13 @@ values or credentials.
6
6
 
7
7
  | Command | Mutates local state | Purpose |
8
8
  | ------------------------------------------------------------ | ------------------- | --------------------------------------------------------- |
9
+ | `rehearsal` or `rehearsal guide` | No by default | Open the guided, state-aware interactive home screen. |
10
+ | `rehearsal setup` | No | Preview safe config, local runtime, and ignore files. |
11
+ | `rehearsal setup --write` | Project files | Create the previewed first-run scaffolding. |
9
12
  | `rehearsal init` | No | Preview safe starter configuration. |
10
13
  | `rehearsal init --write` | Config only | Create config without overwriting. |
14
+ | `rehearsal baseline prepare --records= --ledger=` | No | Preview a fail-closed policy draft from local shape. |
15
+ | `rehearsal baseline prepare --records= --ledger= --write` | Policy only | Write the draft without exposing row values. |
11
16
  | `rehearsal baseline create --records=<path> --ledger=<path>` | Artifact only | Activate a baseline from explicit safe local inputs. |
12
17
  | `rehearsal doctor` | No | Check dependencies, inputs, and safety barriers. |
13
18
  | `rehearsal explain` | No | Print the immutable execution plan. |
@@ -24,12 +29,30 @@ values or credentials.
24
29
  | `rehearsal stop` | Runtime only | Stop this project's local services. |
25
30
  | `rehearsal discard` | Yes, local only | Remove only this project's disposable runtime and volume. |
26
31
 
27
- `run` requires the digest from the current candidate set. Any added, removed, reordered,
28
- or edited migration changes the digest and invalidates the confirmation.
32
+ In an interactive terminal, `run` and `migrate` display the candidate files and ask for
33
+ confirmation before touching the runtime. In automation, they require the digest from
34
+ the current candidate set. Any added, removed, reordered, or edited migration changes
35
+ the digest and invalidates either form of confirmation.
29
36
 
30
37
  `baseline create` never extracts data. The NDJSON and migration-ledger files must already
31
38
  exist inside the project and be safe to retain. Add `--assets=<manifest.json>` to include
32
39
  bounded local Storage bytes; every manifest `file` must also remain inside the project.
40
+ The guided home screen can ask for these paths so they do not need to be supplied as
41
+ flags.
42
+
43
+ `baseline prepare` reads only table and column names from the NDJSON records; row values
44
+ are never included in its result. Its generated policy deliberately marks every column
45
+ decision `REVIEW REQUIRED` and cannot be activated until a human completes the metadata
46
+ and removes the `draft` marker. Rehearsal binds the reviewed policy checksum to the
47
+ baseline and rejects later policy changes during planning and restore.
33
48
 
34
49
  Automation should use `--json` and inspect both exit status and the versioned envelope.
35
50
  Exit-code meanings are documented in the root README. Scripts must not parse human text.
51
+ When standard input or output is not an interactive terminal, bare `rehearsal` prints
52
+ help instead of prompting. Use `--plain` to disable decorative terminal styling.
53
+
54
+ `setup` chooses an available local port block and generates a conservative Supabase
55
+ configuration with hosted access and optional networked services disabled. It does not
56
+ overwrite an existing Rehearsal config, dedicated Supabase config, or concurrently
57
+ changed `.gitignore`. After writing, it includes a Doctor readiness summary. `init`
58
+ remains available for config-only/manual onboarding.
@@ -34,18 +34,30 @@ its labeled local runtime. It does not need a hosted Supabase project or credent
34
34
 
35
35
  ## 1. Install and initialize
36
36
 
37
+ Confirm this shell is running the supported Node.js major before installing:
38
+
39
+ ```bash
40
+ node --version # v24.x
41
+ ```
42
+
37
43
  ```bash
38
44
  npm install --save-dev @rehearsal-db/core@beta
39
- npx rehearsal init
45
+ npx rehearsal
40
46
  ```
41
47
 
42
- `init` is a preview. Read the generated configuration, then explicitly write it:
48
+ Choose **Set up Rehearsal** in the guide. It previews a versioned configuration, a
49
+ dedicated local-only Supabase configuration using available ports, and protective
50
+ `.gitignore` entries before asking permission to write.
51
+
52
+ The same flow is available noninteractively as an explicit preview and write:
43
53
 
44
54
  ```bash
45
- npx rehearsal init --write
55
+ npx rehearsal setup
56
+ npx rehearsal setup --write
46
57
  ```
47
58
 
48
- Rehearsal never overwrites an existing configuration.
59
+ Rehearsal never overwrites an existing configuration. For config-only/manual setup, use
60
+ `npx rehearsal init` followed by `npx rehearsal init --write`.
49
61
 
50
62
  The generated configuration is intentionally incomplete until you review its ports,
51
63
  project ID, application commands, and project-owned input paths. Do not run `doctor`
@@ -53,14 +65,14 @@ until the next section's files exist.
53
65
 
54
66
  ## 2. Add the project-owned inputs
55
67
 
56
- Create the paths named by `rehearsal.config.mjs`:
68
+ Review or create the project-owned inputs named by `rehearsal.config.mjs`:
57
69
 
58
- - a dedicated local Supabase `config.toml`;
70
+ - the dedicated local Supabase `config.toml` (`setup` creates a conservative one);
59
71
  - a sanitization policy describing every exported field;
60
72
  - an active baseline below `.rehearsal/`;
61
73
  - an application proof command that exits nonzero when the restored app is wrong.
62
74
 
63
- For the generated default paths:
75
+ If you used config-only `init`, also create the dedicated Supabase config manually:
64
76
 
65
77
  ```bash
66
78
  mkdir -p infrastructure/rehearsal/supabase infrastructure/rehearsal rehearsal
@@ -79,6 +91,22 @@ Start with synthetic rows shaped like your schema. Do not start onboarding with
79
91
  production data. The following SQL, policy, row, and ledger form one matched example;
80
92
  do not mix them with differently shaped snippets.
81
93
 
94
+ If you already have the safe NDJSON rows and migration ledger, Rehearsal can enumerate
95
+ their table and column shape into a fail-closed policy draft without printing values:
96
+
97
+ ```bash
98
+ npx rehearsal baseline prepare \
99
+ --records=rehearsal/synthetic-data.ndjson \
100
+ --ledger=rehearsal/migration-ledger.json
101
+ npx rehearsal baseline prepare \
102
+ --records=rehearsal/synthetic-data.ndjson \
103
+ --ledger=rehearsal/migration-ledger.json \
104
+ --write
105
+ ```
106
+
107
+ Review every `REVIEW REQUIRED` field, replace it with correct metadata, and remove
108
+ `"draft": true` only after that review. Rehearsal refuses to activate a draft.
109
+
82
110
  Historical migration `supabase/migrations/20260101000000_create_widgets.sql`:
83
111
 
84
112
  ```sql
@@ -13,6 +13,12 @@ Every exported field should receive one action:
13
13
 
14
14
  Fail if a new column is unclassified. Do not default unknown fields to `KEEP`.
15
15
 
16
+ For runtime restore, each included column must also classify whether it is generated,
17
+ whether it is an identity column, and its foreign-key target or explicit absence. Run
18
+ `rehearsal baseline prepare` to create a shape-only draft from synthetic NDJSON. The
19
+ draft uses `REVIEW REQUIRED` placeholders and cannot be activated until they are
20
+ replaced and the `draft` marker is removed.
21
+
16
22
  The package exports `validateSanitizationCoverage` and
17
23
  `applySanitizationAction` as generic primitives:
18
24
 
@@ -67,3 +73,7 @@ Before activation, verify:
67
73
 
68
74
  Completeness is not correctness. Human review of the policy and export boundary remains
69
75
  mandatory before any real source is introduced.
76
+
77
+ The exact reviewed policy bytes are checksum-bound to the active baseline. Planning,
78
+ candidate inspection, and runtime restore refuse to continue if that file later changes;
79
+ build a new reviewed baseline instead of editing the active policy in place.
@@ -1,5 +1,19 @@
1
1
  # Troubleshooting
2
2
 
3
+ ## Setup says Node.js 24 is required
4
+
5
+ Rehearsal intentionally supports one maintained Node.js major in its first beta. Switch
6
+ the current shell before installing or running it. With nvm:
7
+
8
+ ```bash
9
+ nvm install 24
10
+ nvm use 24
11
+ node --version
12
+ ```
13
+
14
+ Then reinstall Rehearsal in the consuming project. The guided CLI checks this before it
15
+ writes setup files.
16
+
3
17
  ## Doctor says Docker is unavailable
4
18
 
5
19
  Start Docker Desktop or Colima, confirm `docker info`, then rerun `rehearsal doctor`.
@@ -7,8 +21,10 @@ Restarting the computer is rarely necessary.
7
21
 
8
22
  ## A port is already in use
9
23
 
10
- Choose three unique non-privileged ports in config and mirror them in the dedicated
11
- Supabase config. Do not stop an unrelated database to make the default ports fit.
24
+ Rerun `rehearsal setup` to select a different available block. Setup checks both active
25
+ listeners and whether every selected port can be bound, then checks again before writing.
26
+ For an existing configuration, choose unique non-privileged ports and mirror them in the
27
+ dedicated Supabase config. Do not stop an unrelated database to make defaults fit.
12
28
 
13
29
  ## Baseline checksum mismatch
14
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehearsal-db/core",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.3",
4
4
  "private": false,
5
5
  "description": "Safely rehearse Supabase migrations against sanitized, production-shaped PostgreSQL data.",
6
6
  "repository": {
@@ -19,6 +19,7 @@ import {
19
19
  createMigrationReplayReceipt,
20
20
  readMigrationSourceBundle,
21
21
  } from "./migration_history.mjs";
22
+ import { validateRuntimeSanitizationPolicy } from "./sanitization_policy.mjs";
22
23
 
23
24
  const resolveProjectInput = (projectRoot, value, label) => {
24
25
  if (typeof value !== "string" || value.trim() === "") {
@@ -103,10 +104,9 @@ export const createSyntheticBaselineFromFiles = async ({
103
104
  })),
104
105
  )
105
106
  : [];
106
- const policy = JSON.parse(policyBytes.toString("utf8"));
107
- if (!Array.isArray(policy.tables)) {
108
- throw new Error("The configured sanitization policy does not list tables.");
109
- }
107
+ const policy = validateRuntimeSanitizationPolicy(
108
+ JSON.parse(policyBytes.toString("utf8")),
109
+ );
110
110
  const expectedTables = policy.tables
111
111
  .filter((table) => table.sourceRows !== "EXCLUDE")
112
112
  .map((table) => table.name);
@@ -117,6 +117,11 @@ export const createSyntheticBaselineFromFiles = async ({
117
117
  JSON.parse(ledgerBytes.toString("utf8")),
118
118
  "Synthetic migration ledger",
119
119
  );
120
+ if (policy.migrationCutoff !== sourceMigrationHistory.at(-1).version) {
121
+ throw new Error(
122
+ `The sanitization policy cutoff ${policy.migrationCutoff} does not match migration ledger cutoff ${sourceMigrationHistory.at(-1).version}.`,
123
+ );
124
+ }
120
125
  const migrationFiles = await readMigrationSourceBundle({
121
126
  directory: new URL(
122
127
  "./",
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Purpose: Inspect explicit local synthetic records and migration evidence, then
3
+ * create a fail-closed sanitization-policy draft for human review.
4
+ */
5
+
6
+ import { createReadStream } from "node:fs";
7
+ import { access, mkdir, open, readFile } from "node:fs/promises";
8
+ import { createInterface } from "node:readline";
9
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
10
+ import { loadRehearsalConfig } from "./configuration.mjs";
11
+ import { buildMigrationLedgerInventory } from "./migration_history.mjs";
12
+
13
+ const identifierPattern = /^[a-z][a-z0-9_]{0,62}$/u;
14
+
15
+ const resolveProjectInput = (projectRoot, value, label) => {
16
+ if (typeof value !== "string" || value.trim() === "") {
17
+ throw new Error(`${label} is required.`);
18
+ }
19
+ const path = resolve(projectRoot, value);
20
+ const owned = relative(projectRoot, path);
21
+ if (
22
+ owned === "" ||
23
+ owned === ".." ||
24
+ owned.startsWith(`..${sep}`) ||
25
+ isAbsolute(owned)
26
+ ) {
27
+ throw new Error(`${label} must be a file inside the project root.`);
28
+ }
29
+ return path;
30
+ };
31
+
32
+ const pathExists = (path) =>
33
+ access(path)
34
+ .then(() => true)
35
+ .catch((error) => {
36
+ if (error?.code === "ENOENT") return false;
37
+ throw error;
38
+ });
39
+
40
+ const inspectSyntheticRecords = async (path) => {
41
+ const tables = new Map();
42
+ let rowCount = 0;
43
+ const lines = createInterface({
44
+ input: createReadStream(path, { encoding: "utf8" }),
45
+ crlfDelay: Infinity,
46
+ });
47
+ for await (const line of lines) {
48
+ if (!line.trim()) continue;
49
+ let record;
50
+ try {
51
+ record = JSON.parse(line);
52
+ } catch (error) {
53
+ throw new Error("Synthetic baseline input contains invalid NDJSON.", {
54
+ cause: error,
55
+ });
56
+ }
57
+ if (
58
+ !record ||
59
+ !identifierPattern.test(record.table ?? "") ||
60
+ !record.row ||
61
+ typeof record.row !== "object" ||
62
+ Array.isArray(record.row)
63
+ ) {
64
+ throw new Error("A synthetic baseline record has an invalid shape.");
65
+ }
66
+ const columns = Object.keys(record.row);
67
+ if (
68
+ columns.length === 0 ||
69
+ columns.some((column) => !identifierPattern.test(column))
70
+ ) {
71
+ throw new Error(
72
+ `Synthetic baseline table ${record.table} has invalid or empty columns.`,
73
+ );
74
+ }
75
+ const table = tables.get(record.table) ?? {
76
+ name: record.table,
77
+ rowCount: 0,
78
+ columns: new Set(),
79
+ };
80
+ table.rowCount += 1;
81
+ for (const column of columns) table.columns.add(column);
82
+ tables.set(record.table, table);
83
+ rowCount += 1;
84
+ }
85
+ if (rowCount === 0) {
86
+ throw new Error("Synthetic baseline input contains no records.");
87
+ }
88
+ return {
89
+ rowCount,
90
+ tables: [...tables.values()]
91
+ .sort((left, right) => left.name.localeCompare(right.name))
92
+ .map((table) => ({
93
+ name: table.name,
94
+ rowCount: table.rowCount,
95
+ columns: [...table.columns].sort(),
96
+ })),
97
+ };
98
+ };
99
+
100
+ const renderPolicyDraft = ({ migrationCutoff, tables }) =>
101
+ `${JSON.stringify(
102
+ {
103
+ policyVersion: 1,
104
+ draft: true,
105
+ migrationCutoff,
106
+ tables: tables.map((table) => ({
107
+ name: table.name,
108
+ group: "synthetic",
109
+ sourceRows: "STREAM AND SANITIZE",
110
+ columns: table.columns.map((name) => ({
111
+ name,
112
+ action: "REVIEW REQUIRED",
113
+ generated: "REVIEW REQUIRED",
114
+ identity: "REVIEW REQUIRED",
115
+ foreignKey: "REVIEW REQUIRED",
116
+ })),
117
+ })),
118
+ },
119
+ null,
120
+ 2,
121
+ )}\n`;
122
+
123
+ export const planBaselinePreparation = async ({
124
+ projectRoot = process.cwd(),
125
+ configPath,
126
+ recordsPath,
127
+ ledgerPath,
128
+ }) => {
129
+ const loaded = await loadRehearsalConfig({ projectRoot, configPath });
130
+ const records = resolveProjectInput(
131
+ loaded.projectRoot,
132
+ recordsPath,
133
+ "Synthetic records path",
134
+ );
135
+ const ledger = resolveProjectInput(
136
+ loaded.projectRoot,
137
+ ledgerPath,
138
+ "Migration ledger path",
139
+ );
140
+ const [inspection, ledgerRows] = await Promise.all([
141
+ inspectSyntheticRecords(records),
142
+ readFile(ledger, "utf8").then(JSON.parse),
143
+ ]);
144
+ const migrationHistory = buildMigrationLedgerInventory(
145
+ ledgerRows,
146
+ "Synthetic migration ledger",
147
+ );
148
+ const destination = loaded.paths.sanitizationPolicy;
149
+ if (await pathExists(destination)) {
150
+ throw new Error(
151
+ `Rehearsal baseline preparation will not overwrite ${relative(loaded.projectRoot, destination)}.`,
152
+ );
153
+ }
154
+ const migrationCutoff = migrationHistory.at(-1).version;
155
+ return {
156
+ projectRoot: loaded.projectRoot,
157
+ destination,
158
+ destinationRelative: relative(loaded.projectRoot, destination),
159
+ recordsPath: relative(loaded.projectRoot, records),
160
+ ledgerPath: relative(loaded.projectRoot, ledger),
161
+ migrationCutoff,
162
+ migrationCount: migrationHistory.length,
163
+ rowCount: inspection.rowCount,
164
+ tables: inspection.tables,
165
+ content: renderPolicyDraft({
166
+ migrationCutoff,
167
+ tables: inspection.tables,
168
+ }),
169
+ };
170
+ };
171
+
172
+ export const applyBaselinePreparation = async (plan) => {
173
+ if (await pathExists(plan.destination)) {
174
+ throw new Error(
175
+ `Rehearsal baseline preparation will not overwrite ${plan.destinationRelative}.`,
176
+ );
177
+ }
178
+ await mkdir(dirname(plan.destination), { recursive: true, mode: 0o700 });
179
+ const handle = await open(plan.destination, "wx", 0o600);
180
+ try {
181
+ await handle.writeFile(plan.content);
182
+ await handle.sync();
183
+ } finally {
184
+ await handle.close();
185
+ }
186
+ };
187
+
188
+ export const summarizeBaselinePreparation = (plan, { mode }) => ({
189
+ mode,
190
+ destination: plan.destinationRelative,
191
+ recordsPath: plan.recordsPath,
192
+ ledgerPath: plan.ledgerPath,
193
+ migrationCutoff: plan.migrationCutoff,
194
+ migrationCount: plan.migrationCount,
195
+ rowCount: plan.rowCount,
196
+ tables: plan.tables,
197
+ nextAction:
198
+ mode === "written"
199
+ ? `Review every REVIEW REQUIRED decision in ${plan.destinationRelative}, remove draft only after review, then create the baseline.`
200
+ : "Review this schema-only summary, then rerun with --write to create the draft.",
201
+ });
@@ -51,7 +51,13 @@ export declare const loadRehearsalConfig: (options?: {
51
51
  export declare const inspectDetectedProject: (options?: {
52
52
  projectRoot?: string;
53
53
  }) => Promise<unknown>;
54
- export declare const renderDetectedConfig: (detected: unknown) => string;
54
+ export declare const renderDetectedConfig: (
55
+ detected: unknown,
56
+ options?: {
57
+ applicationUrl?: string;
58
+ ports?: { api: number; database: number; studio: number };
59
+ },
60
+ ) => string;
55
61
  export declare const SANITIZATION_ACTIONS: Readonly<{
56
62
  KEEP: "KEEP";
57
63
  PSEUDONYMIZE: "PSEUDONYMIZE";
@@ -75,6 +81,13 @@ export declare const validateSanitizationCoverage: (options: {
75
81
  columnCount: number;
76
82
  tables: ReadonlyArray<Record<string, unknown>>;
77
83
  }>;
84
+ export declare const validateRuntimeSanitizationPolicy: (
85
+ policy: unknown,
86
+ ) => unknown;
87
+ export declare const readBoundRuntimeSanitizationPolicy: (options: {
88
+ bytes: Uint8Array | string;
89
+ expectedSha256: string;
90
+ }) => unknown;
78
91
  export declare const applySanitizationAction: (options: {
79
92
  action: string;
80
93
  value: unknown;
@@ -13,7 +13,9 @@ export {
13
13
  SANITIZATION_ACTIONS,
14
14
  applySanitizationAction,
15
15
  normalizeSanitizationAction,
16
+ readBoundRuntimeSanitizationPolicy,
16
17
  validateSanitizationCoverage,
18
+ validateRuntimeSanitizationPolicy,
17
19
  } from "./sanitization_policy.mjs";
18
20
 
19
21
  export const REHEARSAL_CONFIG_VERSION = 1;
@@ -494,12 +496,15 @@ export const inspectDetectedProject = async ({
494
496
  ? "yarn"
495
497
  : "npm";
496
498
  const scripts = packageJson.scripts ?? {};
499
+ const normalizedProjectName = String(packageJson.name ?? basename(root))
500
+ .toLowerCase()
501
+ .replace(/[^a-z0-9-]+/gu, "-")
502
+ .replace(/^-|-$/gu, "");
503
+ const projectName =
504
+ normalizedProjectName.slice(0, 52).replace(/-$/u, "") ||
505
+ "rehearsal-project";
497
506
  return {
498
- projectName:
499
- String(packageJson.name ?? basename(root))
500
- .toLowerCase()
501
- .replace(/[^a-z0-9-]+/gu, "-")
502
- .replace(/^-|-$/gu, "") || "rehearsal-project",
507
+ projectName,
503
508
  packageManager,
504
509
  hasSupabaseConfig: await hasPath("supabase/config.toml"),
505
510
  hasMigrations: await hasPath("supabase/migrations"),
@@ -515,7 +520,13 @@ export const inspectDetectedProject = async ({
515
520
  };
516
521
  };
517
522
 
518
- export const renderDetectedConfig = (detected) => `// @ts-check
523
+ export const renderDetectedConfig = (
524
+ detected,
525
+ {
526
+ applicationUrl = "http://localhost:5175",
527
+ ports = { api: 58321, database: 58322, studio: 58323 },
528
+ } = {},
529
+ ) => `// @ts-check
519
530
  import { defineRehearsalConfig } from "@rehearsal-db/core";
520
531
 
521
532
  export default defineRehearsalConfig({
@@ -537,11 +548,11 @@ export default defineRehearsalConfig({
537
548
  environmentFile: ".rehearsal/runtime.env",
538
549
  },
539
550
  runtime: {
540
- applicationUrl: "http://localhost:5175",
551
+ applicationUrl: ${JSON.stringify(applicationUrl)},
541
552
  projectId: "${detected.projectName}-rehearsal",
542
- apiPort: 58321,
543
- databasePort: 58322,
544
- studioPort: 58323,
553
+ apiPort: ${ports.api},
554
+ databasePort: ${ports.database},
555
+ studioPort: ${ports.studio},
545
556
  },
546
557
  safety: {
547
558
  allowedHosts: ["127.0.0.1", "::1", "localhost"],
@@ -7,7 +7,7 @@
7
7
  import { createHash } from "node:crypto";
8
8
 
9
9
  export const REHEARSAL_RESULT_VERSION = 1;
10
- export const REHEARSAL_VERSION = "0.1.0-beta.2";
10
+ export const REHEARSAL_VERSION = "0.1.0-beta.3";
11
11
 
12
12
  export const REHEARSAL_EXIT_CODES = Object.freeze({
13
13
  success: 0,
@@ -96,7 +96,9 @@ const inferCategory = (error) => {
96
96
  if (/candidate|migration history diverges/iu.test(message)) {
97
97
  return "migration_candidate_failure";
98
98
  }
99
- if (/baseline|artifact|generation/iu.test(message)) return "baseline_invalid";
99
+ if (/baseline|artifact|generation|sanitization policy/iu.test(message)) {
100
+ return "baseline_invalid";
101
+ }
100
102
  if (/migration|ledger|replay/iu.test(message)) {
101
103
  return "migration_verification_failure";
102
104
  }
@@ -106,7 +108,9 @@ const inferCategory = (error) => {
106
108
  if (/loopback|hosted|unsafe|credential|outbound/iu.test(message)) {
107
109
  return "unsafe_environment";
108
110
  }
109
- if (/docker|supabase|runtime|command|enoent/iu.test(message)) {
111
+ if (
112
+ /docker|supabase|runtime|command|enoent|node\.js|ports?\b/iu.test(message)
113
+ ) {
110
114
  return "runtime_dependency_failure";
111
115
  }
112
116
  return "internal_failure";
@@ -0,0 +1,4 @@
1
+ /** Purpose: Keep human-facing Rehearsal counts grammatically consistent. */
2
+
3
+ export const formatCount = (count, singular, plural = `${singular}s`) =>
4
+ `${count} ${count === 1 ? singular : plural}`;