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

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,52 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## [0.1.0-beta.4] - 2026-10-01
9
+
10
+ ### Added
11
+
12
+ - A persistent guided session with polished terminal prompts, interactive policy review,
13
+ concise runtime receipts, expandable technical details, and real-PTY regression tests.
14
+
15
+ ### Changed
16
+
17
+ - Guided menus now place the next recommended action first and automatically re-inspect
18
+ project state after every completed step while preserving plain and non-TTY modes.
19
+
20
+ ## [0.1.0-beta.3] - 2026-10-01
21
+
22
+ ### Added
23
+
24
+ - A state-aware interactive home screen that guides setup, baseline preparation,
25
+ migration review, runtime management, verification, and cleanup without requiring
26
+ users to memorize commands.
27
+ - A safe `setup` workflow that previews and creates a dedicated local Supabase config,
28
+ chooses an available port block, updates protective ignore rules, and summarizes
29
+ readiness without overwriting project files.
30
+ - A schema-only `baseline prepare` workflow that generates a fail-closed policy draft
31
+ with explicit `REVIEW REQUIRED` decisions and never prints source row values.
32
+ - Interactive confirmation of the exact candidate migration set plus friendly progress
33
+ and timing while a rehearsal runs.
34
+
35
+ ### Changed
36
+
37
+ - Human output now recommends the next useful action, uses compact first-run readiness
38
+ summaries, respects `NO_COLOR` and `--plain`, and renders singular counts correctly.
39
+ - Bare `rehearsal` opens the guide only in a terminal and remains noninteractive and
40
+ script-safe when standard input or output is redirected.
41
+
42
+ ### Fixed
43
+
44
+ - Setup detects unsupported Node.js versions before writing, ignores ports occupied by
45
+ Docker or SSH forwarding, and rechecks its selected ports immediately before commit.
46
+ - Generated project identifiers are bounded to values accepted by the local runtime.
47
+
48
+ ### Security
49
+
50
+ - Reviewed sanitization policy bytes are checksum-bound to the active baseline and are
51
+ revalidated during planning and runtime restore.
52
+ - Draft or incomplete sanitization policies cannot be activated as baselines.
53
+
8
54
  ## [0.1.0-beta.2] - 2026-09-15
9
55
 
10
56
  ### Fixed
@@ -72,7 +118,9 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
72
118
  publication uses short-lived trusted OIDC, and every release tag must already exist on
73
119
  protected `main`.
74
120
 
75
- [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.2...HEAD
121
+ [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.4...HEAD
122
+ [0.1.0-beta.4]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.3...v0.1.0-beta.4
123
+ [0.1.0-beta.3]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.2...v0.1.0-beta.3
76
124
  [0.1.0-beta.2]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.1...v0.1.0-beta.2
77
125
  [0.1.0-beta.1]: https://github.com/Ddupasquier/rehearsal-db/releases/tag/v0.1.0-beta.1
78
126
  [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,34 @@ 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 guide stays open after each action,
112
+ re-inspects the project, and advances to the next useful step. It includes an interactive
113
+ column-by-column policy reviewer, concise completion receipts, and optional technical
114
+ details. The explicit commands remain the stable interface for automation and CI.
115
+
116
+ `setup` previews a conservative first-run scaffold: the Rehearsal configuration, a
117
+ dedicated local-only Supabase configuration on an available port block, and protective
118
+ `.gitignore` entries. It writes only with `--write`, never overwrites project files, and
119
+ does not copy enabled external providers from the application's Supabase configuration.
120
+ After writing, it runs the same readiness checks as `doctor` and shows the remaining
121
+ project-owned inputs. Use `init` when you want to create only the configuration file
122
+ manually.
123
+
124
+ `baseline prepare` inspects only the shape of explicit local synthetic records and writes
125
+ a fail-closed sanitization-policy draft. Every column remains `REVIEW REQUIRED` until a
126
+ human classifies its sanitization action, generated/identity behavior, and foreign key.
127
+ Drafts cannot be activated, and a reviewed policy is checksum-bound to its baseline.
128
+
105
129
  `init` previews a type-aware ESM `rehearsal.config.mjs`; it writes only with `--write`
106
130
  and never overwrites an existing file. The explicit `.mjs` extension makes the generated
107
131
  configuration executable in both CommonJS and ESM projects. Review all detected values.
108
132
  Rehearsal intentionally does not detect, copy, or enable a hosted project.
109
133
 
110
134
  `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:
135
+ same immutable planner and perform no state-changing operations. In a terminal,
136
+ `rehearsal run` displays and confirms the exact candidate set before touching the local
137
+ runtime. Automation supplies the digest explicitly:
113
138
 
114
139
  ```bash
115
140
  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,38 @@ 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
+ The guide is a persistent session: after setup, policy review, baseline creation, or a
44
+ runtime action it re-reads project state and offers the next relevant action. Generated
45
+ policy drafts can be completed interactively without editing JSON. Each column still
46
+ requires explicit action, generated, identity, and foreign-key decisions; the guide does
47
+ not silently infer them.
48
+
49
+ `baseline prepare` reads only table and column names from the NDJSON records; row values
50
+ are never included in its result. Its generated policy deliberately marks every column
51
+ decision `REVIEW REQUIRED` and cannot be activated until a human completes the metadata
52
+ and removes the `draft` marker. Rehearsal binds the reviewed policy checksum to the
53
+ baseline and rejects later policy changes during planning and restore.
33
54
 
34
55
  Automation should use `--json` and inspect both exit status and the versioned envelope.
35
56
  Exit-code meanings are documented in the root README. Scripts must not parse human text.
57
+ When standard input or output is not an interactive terminal, bare `rehearsal` prints
58
+ help instead of prompting. Use `--plain` to disable decorative terminal styling.
59
+ `NO_COLOR` also selects the plain numbered interface. Both interactive modes preserve
60
+ the same safety decisions and cancellation behavior.
61
+
62
+ `setup` chooses an available local port block and generates a conservative Supabase
63
+ configuration with hosted access and optional networked services disabled. It does not
64
+ overwrite an existing Rehearsal config, dedicated Supabase config, or concurrently
65
+ changed `.gitignore`. After writing, it includes a Doctor readiness summary. `init`
66
+ remains available for config-only/manual onboarding.
@@ -34,18 +34,31 @@ 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. The guide remains open afterward
51
+ and recommends the next incomplete stage.
52
+
53
+ The same flow is available noninteractively as an explicit preview and write:
43
54
 
44
55
  ```bash
45
- npx rehearsal init --write
56
+ npx rehearsal setup
57
+ npx rehearsal setup --write
46
58
  ```
47
59
 
48
- Rehearsal never overwrites an existing configuration.
60
+ Rehearsal never overwrites an existing configuration. For config-only/manual setup, use
61
+ `npx rehearsal init` followed by `npx rehearsal init --write`.
49
62
 
50
63
  The generated configuration is intentionally incomplete until you review its ports,
51
64
  project ID, application commands, and project-owned input paths. Do not run `doctor`
@@ -53,14 +66,14 @@ until the next section's files exist.
53
66
 
54
67
  ## 2. Add the project-owned inputs
55
68
 
56
- Create the paths named by `rehearsal.config.mjs`:
69
+ Review or create the project-owned inputs named by `rehearsal.config.mjs`:
57
70
 
58
- - a dedicated local Supabase `config.toml`;
71
+ - the dedicated local Supabase `config.toml` (`setup` creates a conservative one);
59
72
  - a sanitization policy describing every exported field;
60
73
  - an active baseline below `.rehearsal/`;
61
74
  - an application proof command that exits nonzero when the restored app is wrong.
62
75
 
63
- For the generated default paths:
76
+ If you used config-only `init`, also create the dedicated Supabase config manually:
64
77
 
65
78
  ```bash
66
79
  mkdir -p infrastructure/rehearsal/supabase infrastructure/rehearsal rehearsal
@@ -79,6 +92,27 @@ Start with synthetic rows shaped like your schema. Do not start onboarding with
79
92
  production data. The following SQL, policy, row, and ledger form one matched example;
80
93
  do not mix them with differently shaped snippets.
81
94
 
95
+ If you already have the safe NDJSON rows and migration ledger, Rehearsal can enumerate
96
+ their table and column shape into a fail-closed policy draft without printing values:
97
+
98
+ ```bash
99
+ npx rehearsal baseline prepare \
100
+ --records=rehearsal/synthetic-data.ndjson \
101
+ --ledger=rehearsal/migration-ledger.json
102
+ npx rehearsal baseline prepare \
103
+ --records=rehearsal/synthetic-data.ndjson \
104
+ --ledger=rehearsal/migration-ledger.json \
105
+ --write
106
+ ```
107
+
108
+ After creating the draft, choose **Review the script** in the guide. Rehearsal walks each
109
+ column through sanitization action, generated status, identity status, and optional
110
+ foreign-key metadata. Every answer is explicit, the complete policy is validated before
111
+ writing, and the original draft is replaced only if it did not change during review.
112
+
113
+ For noninteractive workflows, review every `REVIEW REQUIRED` field directly and remove
114
+ `"draft": true` only after that review. Rehearsal refuses to activate a draft.
115
+
82
116
  Historical migration `supabase/migrations/20260101000000_create_widgets.sql`:
83
117
 
84
118
  ```sql
package/docs/releasing.md CHANGED
@@ -37,7 +37,7 @@ publication must change and prove the workflow before narrowing that permission.
37
37
  onboarding. Correct and retest the first confusing, missing, or wrong instruction.
38
38
  5. Change `private` to `false` only in the reviewed release change.
39
39
  6. Record the exact tarball filename, SHA-1, SHA-256, allowlisted files, unpacked size,
40
- executable, and zero-runtime-dependency result.
40
+ executable, declared runtime dependency inventory, and dependency audit result.
41
41
  7. Obtain explicit publication authorization for that exact version and artifact.
42
42
  8. Merge the approved release commit through protected `main` and create the exact
43
43
  `v<package-version>` tag and GitHub release.
@@ -13,6 +13,18 @@ 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
+
22
+ In an interactive terminal, the guided **Review the script** action completes these
23
+ decisions one column at a time. Replacement-oriented actions are shown first, retaining
24
+ a value is explicitly labeled as sensitive, and no choice is silently inferred. The
25
+ reviewer validates the completed policy and refuses to overwrite a draft changed during
26
+ the session.
27
+
16
28
  The package exports `validateSanitizationCoverage` and
17
29
  `applySanitizationAction` as generic primitives:
18
30
 
@@ -67,3 +79,7 @@ Before activation, verify:
67
79
 
68
80
  Completeness is not correctness. Human review of the policy and export boundary remains
69
81
  mandatory before any real source is introduced.
82
+
83
+ The exact reviewed policy bytes are checksum-bound to the active baseline. Planning,
84
+ candidate inspection, and runtime restore refuse to continue if that file later changes;
85
+ 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.4",
4
4
  "private": false,
5
5
  "description": "Safely rehearse Supabase migrations against sanitized, production-shaped PostgreSQL data.",
6
6
  "repository": {
@@ -69,8 +69,11 @@
69
69
  "database"
70
70
  ],
71
71
  "license": "MIT",
72
- "dependencies": {},
72
+ "dependencies": {
73
+ "@clack/prompts": "^1.8.1"
74
+ },
73
75
  "devDependencies": {
76
+ "@lydell/node-pty": "^1.2.0-beta.15",
74
77
  "prettier": "^3.9.6",
75
78
  "vitest": "^4.1.10"
76
79
  }
@@ -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"],