@rehearsal-db/core 1.0.2 → 1.2.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 (64) hide show
  1. package/CHANGELOG.md +56 -4
  2. package/COMPATIBILITY.md +41 -4
  3. package/README.md +8 -4
  4. package/dist/package.json +3 -1
  5. package/dist/src/cli/arguments.mjs +1 -0
  6. package/dist/src/cli/basic_command_handlers.mjs +4 -0
  7. package/dist/src/cli/editor_command_handlers.mjs +24 -0
  8. package/dist/src/project/configuration_contract.d.mts +1 -0
  9. package/dist/src/project/configuration_normalization.mjs +2 -1
  10. package/dist/src/project/configuration_rendering.mjs +3 -2
  11. package/dist/src/project/configuration_resolution.mjs +0 -2
  12. package/dist/src/project/project_detection.d.mts +2 -0
  13. package/dist/src/project/project_detection.mjs +1 -0
  14. package/dist/src/project/runtime_project_identity.d.mts +5 -0
  15. package/dist/src/project/runtime_project_identity.mjs +41 -0
  16. package/dist/src/project/setup.mjs +3 -1
  17. package/dist/src/runtime/migration_history.d.mts +5 -1
  18. package/dist/src/runtime/migration_history.mjs +24 -6
  19. package/dist/src/runtime/migration_impact.d.mts +125 -0
  20. package/dist/src/runtime/migration_impact.mjs +278 -0
  21. package/dist/src/runtime/migration_impact_catalog.d.mts +18 -0
  22. package/dist/src/runtime/migration_impact_catalog.mjs +116 -0
  23. package/dist/src/runtime/migration_impact_plan.d.mts +47 -0
  24. package/dist/src/runtime/migration_impact_plan.mjs +297 -0
  25. package/dist/src/runtime/migration_impact_queries.d.mts +22 -0
  26. package/dist/src/runtime/migration_impact_queries.mjs +270 -0
  27. package/dist/src/runtime/migration_insights.d.mts +137 -0
  28. package/dist/src/runtime/migration_insights.mjs +143 -0
  29. package/dist/src/runtime/migration_insights_input.d.mts +13 -0
  30. package/dist/src/runtime/migration_insights_input.mjs +153 -0
  31. package/dist/src/runtime/migration_sql_lexer.d.mts +14 -0
  32. package/dist/src/runtime/migration_sql_lexer.mjs +204 -0
  33. package/dist/src/runtime/migration_sql_scan.d.mts +16 -0
  34. package/dist/src/runtime/migration_sql_scan.mjs +369 -0
  35. package/dist/src/runtime/plan.d.mts +2 -1
  36. package/dist/src/runtime/plan.mjs +4 -4
  37. package/dist/src/runtime/policy.d.mts +2 -0
  38. package/dist/src/runtime/policy.mjs +13 -1
  39. package/dist/src/runtime/privileges.d.mts +25 -0
  40. package/dist/src/runtime/privileges.mjs +198 -0
  41. package/dist/src/runtime/topology.d.mts +1 -1
  42. package/dist/src/runtime/topology.mjs +2 -2
  43. package/dist/src/shared/cancellation.mjs +1 -0
  44. package/dist/src/shared/held_path_ancestry.d.mts +7 -0
  45. package/dist/src/shared/held_path_ancestry.mjs +110 -0
  46. package/dist/src/shared/operation_guard.mjs +1 -0
  47. package/dist/src/targets/migration_impact_runtime.d.mts +22 -0
  48. package/dist/src/targets/migration_impact_runtime.mjs +367 -0
  49. package/dist/src/targets/supabase.mjs +1 -0
  50. package/docs/README.md +20 -7
  51. package/docs/adapters.md +10 -0
  52. package/docs/architecture.md +75 -1
  53. package/docs/commands.md +169 -34
  54. package/docs/configuration.md +42 -9
  55. package/docs/getting-started.md +4 -4
  56. package/docs/index.md +1 -0
  57. package/docs/releasing.md +15 -11
  58. package/docs/roadmap.md +49 -9
  59. package/docs/runtime-lifecycle.md +15 -4
  60. package/docs/runtime-policies.md +145 -0
  61. package/docs/stability.md +8 -4
  62. package/docs/updating.md +73 -0
  63. package/docs/vscode.md +30 -19
  64. package/package.json +3 -1
package/CHANGELOG.md CHANGED
@@ -1,9 +1,60 @@
1
1
  # Changelog
2
2
 
3
- All notable Rehearsal package changes will be documented here. The format follows Keep
4
- a Changelog, and versions will follow Semantic Versioning after the package exists.
3
+ All notable Rehearsal package changes are documented here. The format follows Keep
4
+ a Changelog, and published versions follow Semantic Versioning.
5
5
 
6
- ## [Unreleased]
6
+ ## [1.2.0] - 2026-10-24
7
+
8
+ ### Added
9
+
10
+ - An optional `migrationImpact: 1` contract: offline `inspect impact` and explicitly
11
+ confirmed `measure impact` for one selected, already-verified disposable local
12
+ sandbox. Bounded read-only snapshot aggregates cover narrow supported row,
13
+ populated/NULL, duplicate and foreign-key orphan counts; unsupported or incomplete
14
+ evidence stays unknown. No pending migration is executed, no runtime is started
15
+ or repaired, and no SQL, object names or row contents are returned. Counts are
16
+ not sequential simulation, production prediction or a safety verdict. Existing
17
+ project locking/cancellation is reused without adding a rehearsal gate or CLI fee.
18
+ - An optional versioned `inspect insights` contract for bounded, read-only static
19
+ migration warnings. Findings bind to exact pending file bytes and the complete
20
+ existing candidate digest, with fixed explanations and lexical locations.
21
+ Unsupported, malformed or truncated analysis remains unknown; no detected
22
+ warning is never SQL validity, database verification or a safety verdict.
23
+ - Project-local input bounds, unchanged leaf/directory identity checks and shared
24
+ input hash binding. Inspection neither executes SQL nor starts an engine or
25
+ contacts a database. Existing migration-plan v1 and failure diagnosis remain
26
+ separate, unchanged contracts.
27
+
28
+ ### Documentation
29
+
30
+ - Clarify reviewed PostgreSQL/application/full service dependencies and resource
31
+ measurement limits without adding service exclusions or a new acceptance gate.
32
+ - Distinguish project runtime hooks from native migration-tool history support.
33
+
34
+ ## [1.1.0] - 2026-10-21
35
+
36
+ ### Added
37
+
38
+ - Optional read-only runtime privilege expectations for reviewed schemas, tables,
39
+ sequences and exact function signatures. Required and forbidden effective access is
40
+ separate from RLS, includes inherited and column-only permissions, and never repairs
41
+ grants automatically. Existing policies without this field keep their behavior.
42
+ - Document explicit least-privilege Data API grants, forward-only correction and
43
+ positive/negative API proofs for Supabase's October 30 opt-in table-exposure change.
44
+
45
+ ### Fixed
46
+
47
+ - Record the Supabase provider in newly written runtime receipts, so inventory can
48
+ distinguish recorded format/provider compatibility from unknown legacy provenance.
49
+ Older receipts remain unknown rather than being silently rewritten or reverified.
50
+ - Bound newly generated Supabase project IDs to the provider's 40-character limit
51
+ with a deterministic suffix. Preserve valid existing identities; reject invalid
52
+ configured IDs without renaming or adopting resources.
53
+ - Align OS setup helpers with core's Node.js 22, 24 and 26 support. macOS/Linux
54
+ helpers accept an explicit `REHEARSAL_NODE_PATH`, and missing-Node guidance points
55
+ to version-manager selection or the official installer without installing an OS tool.
56
+ - Update compatible older CLI installations to the helper's exact current target
57
+ instead of treating every 1.0.1-or-newer installation as already current.
7
58
 
8
59
  ## [1.0.2] - 2026-10-20
9
60
 
@@ -815,7 +866,8 @@ not unattended cleanup, and container age is not proof of user inactivity.
815
866
  publication uses short-lived trusted OIDC, and every release tag must already exist on
816
867
  protected `main`.
817
868
 
818
- [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.2...HEAD
869
+ [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.1.0...HEAD
870
+ [1.1.0]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.2...v1.1.0
819
871
  [1.0.2]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.1...v1.0.2
820
872
  [1.0.1]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.0...v1.0.1
821
873
  [1.0.0]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.10...v1.0.0
package/COMPATIBILITY.md CHANGED
@@ -26,12 +26,49 @@ The first `1.0.0` release requires external beta evidence, a support policy and
26
26
  settled public API described in [the stability policy](docs/stability.md). Supporting
27
27
  additional database families, package managers or operating systems is not implied.
28
28
 
29
- Rehearsal supports maintained even-numbered Node.js release lines that pass its CI
30
- matrix. Rehearsal 1.0 supports Node.js 22, 24, and 26; Node.js 24 LTS is the recommended
31
- default and the release-build runtime. End-of-life and odd-numbered Node.js releases are
32
- not supported. A future release may remove a Node.js line after its upstream end-of-life
29
+ Rehearsal supports the maintained Node.js release lines verified by its existing CI
30
+ matrix: Node.js 22, 24, and 26. Node.js 24 LTS is the recommended default and the
31
+ release-build runtime. End-of-life and unverified future release lines are not
32
+ supported. A future release may remove a Node.js line after its upstream end-of-life
33
33
  date, with that change called out in the changelog.
34
34
 
35
+ Core 1.1.0's OS helpers accept the same three lines. A terminal or desktop launcher
36
+ that cannot find Node does not mean an installed version is unsupported. Select it
37
+ through the project's existing version manager or use the helper's explicit executable
38
+ path; see [OS setup](installers/README.md). Helpers never install Node or switch a
39
+ project's runtime automatically.
40
+
41
+ The 1.2.0 source candidate adds optional capability
42
+ `rehearsalContracts.migrationInsights: 1` and `inspect insights --json`. It preserves
43
+ existing migration-plan v1, baseline formats, digest meaning and failure diagnosis.
44
+ Consumers must check the capability and exact plan/file bindings; absent or
45
+ unavailable static analysis is not safety evidence. This candidate version and its
46
+ setup target do not establish publication or completed editor/native acceptance.
47
+
48
+ Further source work adds optional `rehearsalContracts.migrationImpact: 1` with
49
+ `inspect impact` and `measure impact`. Consumers must explicitly select a configured
50
+ local target and bind measurement consent to the exact whole-plan digest. Preview
51
+ does not contact a database; measurement reads only the current verified sandbox,
52
+ without applying migrations, starting/repairing a runtime or changing database data.
53
+ Aggregate execution requires PostgreSQL 17 or newer with server-side transaction
54
+ deadlines and narrowly supported ordinary-table/built-in semantics; older servers
55
+ remain unknown, with no automatic database upgrade.
56
+ Supabase measurement uses the ownership-proven local container's built-in
57
+ `supabase_admin` socket role so the fixed disk bound and catalog lock can be enforced.
58
+ It does not promote `postgres`, change grants, load credentials or connect remotely.
59
+ Read-only catalog preflight is separate from the count snapshot. Bounded strong
60
+ relation locks precede that snapshot and temporarily block writers and metadata
61
+ changes in the selected disposable sandbox; this is not a nonblocking query promise.
62
+ Counts are decimal strings or `null` with an explicit unknown reason, never a safety
63
+ verdict, sequential simulation or production prediction. Partial/unknown measurement
64
+ uses `not_ready` with the existing not-ready exit `1`; normal rehearsal gates and existing JSON contracts
65
+ are unchanged. This unpublished source capability does not claim a new editor UI,
66
+ new database family or additional platform/native acceptance.
67
+ Older strict editor activity consumers can reject the new `measure impact` operation
68
+ kind, including a retained completed receipt. Pair this capability with an editor
69
+ consumer explicitly supporting that kind; the CLI does not silently rename the
70
+ operation, relax unknown-kind validation or update an active editor installation.
71
+
35
72
  ## Operating systems and execution location
36
73
 
37
74
  Core 1.0.1's platform boundary distinguishes the computer running VS Code from the
package/README.md CHANGED
@@ -47,7 +47,7 @@ it through a reviewed privacy policy, and retire the access again. It is never p
47
47
  production deployment tool.
48
48
 
49
49
  The CLI is free and MIT-licensed, with stable releases on
50
- [npm](https://www.npmjs.com/package/@rehearsal-db/core). This guide targets **1.0.2**;
50
+ [npm](https://www.npmjs.com/package/@rehearsal-db/core). This guide targets **1.2.0**;
51
51
  install only a version actually published there. Prefer a visual workflow?
52
52
  [Install the free VS Code Starter preview](docs/vscode.md). Its included features are
53
53
  free indefinitely for personal or commercial development—no account, card, or trial.
@@ -71,7 +71,7 @@ your migration is intended to change.
71
71
 
72
72
  Prefer OS-specific files? Use the [Windows / WSL 2](installers/windows/README.md),
73
73
  [macOS](installers/macos/README.md) or [Linux](installers/linux/README.md) setup folder.
74
- These readable helpers require existing Node.js 24 or 26 and check prerequisites by
74
+ These readable helpers accept existing Node.js 22, 24 or 26 and check prerequisites by
75
75
  default; installation is an explicit option. They do not install OS tools or start a
76
76
  database. See [their exact behavior and release availability](installers/README.md).
77
77
 
@@ -198,6 +198,7 @@ Start here:
198
198
  - [VS Code extension](docs/vscode.md) — use the project-aware visual workbench
199
199
  - [Safe hands-on tutorial](docs/tutorial.md) — try the full flow in a disposable project
200
200
  - [Troubleshooting](docs/troubleshooting.md) — fix common setup problems
201
+ - [Updates](docs/updating.md) — update the CLI or extension without resetting data
201
202
  - [Next steps](docs/roadmap.md) — see the current product sequence
202
203
 
203
204
  Reference:
@@ -214,9 +215,12 @@ Reference:
214
215
  - [Standalone workflow and security gates](docs/standalone-workflow.md)
215
216
  - [Runtime and local identity policies](docs/runtime-policies.md)
216
217
  - [Glossary](docs/glossary.md)
217
- - [Repository architecture](docs/architecture.md)
218
218
  - [Stability and the first stable release](docs/stability.md)
219
- - [Release process](docs/releasing.md)
219
+
220
+ For maintainers developing and publishing Rehearsal:
221
+
222
+ - [Repository architecture](docs/architecture.md)
223
+ - [Maintainer release process](docs/releasing.md)
220
224
 
221
225
  ## Getting support
222
226
 
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehearsal-db/core",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "private": false,
5
5
  "description": "Catch PostgreSQL and Supabase migrations that pass on empty databases but fail on real data—before deploy.",
6
6
  "repository": {
@@ -27,6 +27,8 @@
27
27
  "resourceInventory": 1,
28
28
  "resourceManager": 1,
29
29
  "migrationReview": 1,
30
+ "migrationInsights": 1,
31
+ "migrationImpact": 1,
30
32
  "configurationReview": 1,
31
33
  "baselineShape": 1
32
34
  },
@@ -150,6 +150,7 @@ export const commandMutatesProject = ({ command, flags, guided, }) => {
150
150
  "open",
151
151
  "start",
152
152
  "migrate",
153
+ "measure impact",
153
154
  "reset",
154
155
  "stop",
155
156
  "discard",
@@ -35,6 +35,9 @@ Commands:
35
35
  inspect baseline Show verified baseline provenance
36
36
  inspect migrations Classify represented, applied, and candidate migrations
37
37
  inspect plan Review ordered migrations, required dependencies, and warnings
38
+ inspect insights Review bounded static SQL warnings; never a safety verdict
39
+ inspect impact Preview aggregate measurements for one explicit --target
40
+ measure impact Measure reviewed aggregates on that running local sandbox
38
41
  start Start an existing verified local runtime
39
42
  migrate --confirm-candidates= Apply the exact candidate suffix without resetting
40
43
  reset Restore and verify the immutable local baseline
@@ -48,6 +51,7 @@ Commands:
48
51
 
49
52
  Options: --help, -h --version, -V --json --verbose --debug --plain
50
53
  --config=<path> --target=<provider for setup; configured name for runtime>
54
+ --confirm-candidates=<whole-plan digest for run, migrate, or measure impact>
51
55
  --confirm-source-access=<digest> --confirm-source-retirement=<digest>
52
56
  --confirm-refresh=<digest>`;
53
57
  export const createBasicCommandHandlers = () => [
@@ -1,9 +1,33 @@
1
1
  /** Versioned, read-only contracts for editor review; never a second runtime engine. */
2
2
  import { inspectMigrationReviewPlan } from "../runtime/migration_plan.mjs";
3
+ import { inspectMigrationInsights, renderMigrationInsights, } from "../runtime/migration_insights.mjs";
4
+ import { inspectMigrationImpact, measureMigrationImpact, renderMigrationImpact, } from "../runtime/migration_impact.mjs";
3
5
  import { exactCommands, renderedCommand } from "./command_contract.mjs";
4
6
  import { applyStopStalePlan, inspectStopStalePlan, stopReviewedStaleTarget, } from "../runtime/stale_environment_plan.mjs";
5
7
  import { inspectConfigurationMetadata, reviewConfigurationProposal, } from "../project/configuration_review.mjs";
6
8
  export const createEditorCommandHandlers = () => [
9
+ exactCommands("migration-impact-preview", ["inspect impact"], async ({ planOptions, flags }) => {
10
+ const data = await inspectMigrationImpact(planOptions, flags.target);
11
+ return renderedCommand({ data, render: renderMigrationImpact });
12
+ }),
13
+ exactCommands("migration-impact-measurement", ["measure impact"], async ({ planOptions, flags }) => {
14
+ const data = await measureMigrationImpact({
15
+ options: planOptions,
16
+ targetName: flags.target,
17
+ confirmation: flags.confirmation,
18
+ });
19
+ const incomplete = ["partial", "unknown"].includes(data.measurementStatus);
20
+ return renderedCommand({
21
+ data,
22
+ render: renderMigrationImpact,
23
+ status: incomplete ? "not_ready" : "success",
24
+ ...(incomplete ? { exitCode: 1 } : {}),
25
+ });
26
+ }),
27
+ exactCommands("migration-insights", ["inspect insights"], async ({ planOptions }) => {
28
+ const data = await inspectMigrationInsights(planOptions);
29
+ return renderedCommand({ data, render: renderMigrationInsights });
30
+ }),
7
31
  exactCommands("configuration", ["configuration", "configuration review"], async ({ command, planOptions, flags }) => {
8
32
  const data = command === "configuration review"
9
33
  ? await reviewConfigurationProposal(planOptions, flags.proposalPath ??
@@ -126,6 +126,7 @@ export interface RehearsalConfig {
126
126
  protected?: boolean;
127
127
  target?: "supabase" | "postgresql";
128
128
  applicationUrl?: string;
129
+ /** Exact lowercase/digit/hyphen ownership ID: 2–40 Supabase, 2–63 PostgreSQL. */
129
130
  projectId?: string;
130
131
  apiPort?: number;
131
132
  databasePort: number;
@@ -9,6 +9,7 @@ import { normalizeResourcePolicy, normalizeRequiredServices, } from "./configura
9
9
  import { normalizeConfigurationProfiles } from "./configuration_profiles.mjs";
10
10
  import { normalizeApplicationUrls } from "./configuration_application_urls.mjs";
11
11
  import { normalizeApplicationActivity } from "./configuration_activity.mjs";
12
+ import { assertRuntimeProjectId } from "./runtime_project_identity.mjs";
12
13
  import { assertBoolean, assertCommand, assertDatabaseIdentifier, assertEnvironmentKey, assertEnvironmentReference, assertHttpStatus, assertIdentifier, assertKnownKeys, assertLoopbackUrl, assertNonEmptyString, assertPlainObject, assertPort, assertPostgresImage, assertPositiveInteger, assertRelativePath, assertStringArray, } from "./configuration_validation.mjs";
13
14
  const LOOPBACK_HOSTS = new Set(REHEARSAL_DEFAULTS.safety.allowedHosts);
14
15
  const SUPPORTED_SUPABASE_OAUTH_PROVIDERS = new Set(["github", "google"]);
@@ -351,7 +352,7 @@ export const normalizeRehearsalConfig = (input) => {
351
352
  protected: assertBoolean(runtime.protected ?? false, "config.runtime.protected"),
352
353
  target: runtimeTarget,
353
354
  applicationUrl: applicationUrls.applicationUrl,
354
- projectId: assertNonEmptyString(runtime.projectId ?? REHEARSAL_DEFAULTS.runtime.projectId, "config.runtime.projectId"),
355
+ projectId: assertRuntimeProjectId(runtime.projectId ?? REHEARSAL_DEFAULTS.runtime.projectId, runtimeTarget),
355
356
  ports: Object.freeze(ports),
356
357
  }),
357
358
  safety: Object.freeze({
@@ -1,4 +1,5 @@
1
1
  /** Render reviewed starter configurations without reading or writing a project. */
2
+ import { createRuntimeProjectId } from "./runtime_project_identity.mjs";
2
3
  export const renderDetectedConfig = (detected, { applicationUrl = "http://localhost:5175", ports = { api: 58321, database: 58322, studio: 58323 }, } = {}) => `// @ts-check
3
4
  /**
4
5
  * Rehearsal configuration for ${detected.projectName}.
@@ -132,7 +133,7 @@ export default defineRehearsalConfig({
132
133
  protected: false, // Pin data against reset, run, migrate, discard, and runtime cleanup.
133
134
  target: "supabase",
134
135
  applicationUrl: ${JSON.stringify(applicationUrl)},
135
- projectId: "${detected.projectName}-rehearsal",
136
+ projectId: ${JSON.stringify(createRuntimeProjectId(detected.projectName, "supabase", detected.projectIdSeed))},
136
137
  apiPort: ${ports.api},
137
138
  databasePort: ${ports.database},
138
139
  studioPort: ${ports.studio},
@@ -266,7 +267,7 @@ export default defineRehearsalConfig({
266
267
  protected: false, // Pin data against reset, run, migrate, discard, and runtime cleanup.
267
268
  target: "postgresql",
268
269
  applicationUrl: ${JSON.stringify(applicationUrl)},
269
- projectId: "${detected.projectName}-rehearsal",
270
+ projectId: ${JSON.stringify(createRuntimeProjectId(detected.projectName, "postgresql", detected.projectIdSeed))},
270
271
  databasePort: ${databasePort},
271
272
  },
272
273
 
@@ -3,7 +3,6 @@ import { basename, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { findRehearsalConfigPath, } from "./configuration_discovery.mjs";
5
5
  import { normalizeRehearsalConfig } from "./configuration_normalization.mjs";
6
- import { assertIdentifier } from "./configuration_validation.mjs";
7
6
  export const loadRehearsalConfig = async ({ projectRoot = process.cwd(), configPath, } = {}) => {
8
7
  const root = resolve(projectRoot);
9
8
  const path = await findRehearsalConfigPath({
@@ -74,7 +73,6 @@ export const loadRehearsalConfig = async ({ projectRoot = process.cwd(), configP
74
73
  throw new Error("config.preparation.privacyKey must resolve to a secret file inside config.baseline.artifactDirectory.");
75
74
  }
76
75
  }
77
- assertIdentifier(config.runtime.projectId, "config.runtime.projectId");
78
76
  return Object.freeze({
79
77
  config,
80
78
  configPath: path,
@@ -1,6 +1,8 @@
1
1
  /** Inspect project-local files used to render a conservative starter config. */
2
2
  export interface DetectedProject {
3
3
  projectName: string;
4
+ /** Untruncated normalized name keeps shortened new runtime identities distinct. */
5
+ projectIdSeed?: string;
4
6
  packageManager: "npm" | "pnpm" | "yarn";
5
7
  hasSupabaseConfig: boolean;
6
8
  hasMigrations: boolean;
@@ -31,6 +31,7 @@ export const inspectDetectedProject = async ({ projectRoot = process.cwd(), } =
31
31
  "rehearsal-project";
32
32
  return {
33
33
  projectName,
34
+ projectIdSeed: normalizedProjectName || projectName,
34
35
  packageManager,
35
36
  hasSupabaseConfig: await hasPath("supabase/config.toml"),
36
37
  hasMigrations: await hasPath("supabase/migrations"),
@@ -0,0 +1,5 @@
1
+ /** Preserve exact configured runtime ownership and bound newly generated identities. */
2
+ import type { RehearsalRuntimeTargetId } from "../targets/target.mjs";
3
+ export declare const assertRuntimeProjectId: (value: unknown, target: RehearsalRuntimeTargetId) => string;
4
+ /** Only new setup identities are shortened; valid existing generated IDs stay stable. */
5
+ export declare const createRuntimeProjectId: (projectName: string, target: RehearsalRuntimeTargetId, identitySeed?: string) => string;
@@ -0,0 +1,41 @@
1
+ /** Preserve exact configured runtime ownership and bound newly generated identities. */
2
+ import { createHash } from "node:crypto";
3
+ import { RehearsalError } from "../shared/diagnostics.mjs";
4
+ import { assertIdentifier, assertNonEmptyString, } from "./configuration_validation.mjs";
5
+ const maximumProjectIdLength = (target) => target === "supabase" ? 40 : 63;
6
+ export const assertRuntimeProjectId = (value, target) => {
7
+ const projectId = assertNonEmptyString(value, "config.runtime.projectId");
8
+ const maximumLength = maximumProjectIdLength(target);
9
+ if (projectId.length < 2 || projectId.length > maximumLength) {
10
+ const provider = target === "supabase" ? "Supabase" : "PostgreSQL";
11
+ throw new RehearsalError({
12
+ category: "configuration_invalid",
13
+ code: "CONFIGURATION_INVALID",
14
+ message: `config.runtime.projectId must contain 2–${maximumLength} characters for ${provider}.`,
15
+ expected: `2–${maximumLength} lowercase letters, digits, or hyphens`,
16
+ actual: { length: projectId.length },
17
+ context: { field: "runtime.projectId", target, maximumLength },
18
+ refused: "No runtime was started or renamed. Rehearsal never truncates a configured project ID or adopts resources by prefix.",
19
+ suggestions: [
20
+ "Choose a distinct, explicitly reviewed ID for a new disposable runtime. Review existing resources before changing their configured identity.",
21
+ ],
22
+ });
23
+ }
24
+ return assertIdentifier(projectId, "config.runtime.projectId");
25
+ };
26
+ /** Only new setup identities are shortened; valid existing generated IDs stay stable. */
27
+ export const createRuntimeProjectId = (projectName, target, identitySeed = projectName) => {
28
+ const candidate = `${projectName}-rehearsal`;
29
+ const maximumLength = maximumProjectIdLength(target);
30
+ if (candidate.length <= maximumLength)
31
+ return assertRuntimeProjectId(candidate, target);
32
+ const digest = createHash("sha256")
33
+ .update(`${identitySeed}-rehearsal`)
34
+ .digest("hex")
35
+ .slice(0, 12);
36
+ const suffix = `-${digest}-rehearsal`;
37
+ const prefix = projectName
38
+ .slice(0, maximumLength - suffix.length)
39
+ .replace(/-+$/u, "");
40
+ return assertRuntimeProjectId(`${prefix}${suffix}`, target);
41
+ };
@@ -8,6 +8,7 @@ import { access, mkdir, open, readFile, rename, rm, writeFile, } from "node:fs/p
8
8
  import { basename, dirname, join, relative, resolve } from "node:path";
9
9
  import { findRehearsalConfigPath, inspectDetectedProject, loadRehearsalConfig, renderDetectedConfig, renderDetectedPostgresqlConfig, } from "./configuration.mjs";
10
10
  import { resolveRuntimeTarget } from "../targets/target.mjs";
11
+ import { createRuntimeProjectId } from "./runtime_project_identity.mjs";
11
12
  export { assertSupportedRehearsalNodeRuntime, inspectRehearsalNodeRuntime, } from "../shared/node_runtime.mjs";
12
13
  import { assertSupportedRehearsalNodeRuntime } from "../shared/node_runtime.mjs";
13
14
  const CONFIG_PATH = "rehearsal.config.mjs";
@@ -257,7 +258,8 @@ export const planRehearsalSetup = async ({ projectRoot = process.cwd(), configPa
257
258
  ...(isPortAvailable ? { isAvailable: isPortAvailable } : {}),
258
259
  });
259
260
  const applicationUrl = existing?.config.runtime.applicationUrl ?? "http://localhost:5175";
260
- const projectId = existing?.config.runtime.projectId ?? `${detected.projectName}-rehearsal`;
261
+ const projectId = existing?.config.runtime.projectId ??
262
+ createRuntimeProjectId(detected.projectName, runtimeTarget, detected.projectIdSeed);
261
263
  const sourceSupabaseConfig = runtimeTarget === "supabase"
262
264
  ? await readOptionalFile(join(root, "supabase/config.toml"))
263
265
  : null;
@@ -24,7 +24,11 @@ export interface MigrationReplayReceipt {
24
24
  entries: MigrationReplayEntry[];
25
25
  historySha256: string;
26
26
  }
27
- export declare const readMigrationFileInventory: (directory: URL) => Promise<MigrationFileEntry[]>;
27
+ export declare const readMigrationFileInventory: (directory: URL, options?: {
28
+ readonly maximumFiles?: number;
29
+ readonly maximumDirectoryEntries?: number;
30
+ readonly readBytes?: (path: URL, filename: string) => Promise<Buffer>;
31
+ }) => Promise<MigrationFileEntry[]>;
28
32
  export declare const readMigrationSourceBundle: ({ directory, sourceLedger, }: {
29
33
  directory: URL;
30
34
  sourceLedger: readonly MigrationLedgerEntry[];
@@ -4,7 +4,7 @@
4
4
  * this module is reusable script infrastructure.
5
5
  */
6
6
  import { createHash } from "node:crypto";
7
- import { readFile, readdir } from "node:fs/promises";
7
+ import { opendir, readFile, readdir } from "node:fs/promises";
8
8
  const migrationNamePattern = /^(\d{14})_(.+)\.sql$/u;
9
9
  const sha256 = (value) => createHash("sha256").update(value).digest("hex");
10
10
  const canonicalJson = (value) => `${JSON.stringify(value, null, "\t")}\n`;
@@ -32,17 +32,35 @@ const assertOrderedUniqueHistory = (entries, label) => {
32
32
  }
33
33
  return entries;
34
34
  };
35
- export const readMigrationFileInventory = async (directory) => {
36
- const names = (await readdir(directory))
37
- .filter((name) => name.endsWith(".sql"))
38
- .sort();
35
+ export const readMigrationFileInventory = async (directory, options = {}) => {
36
+ let names = [];
37
+ if (options.maximumDirectoryEntries === undefined) {
38
+ names = (await readdir(directory)).filter((name) => name.endsWith(".sql"));
39
+ }
40
+ else {
41
+ let visited = 0;
42
+ // Async directory iteration closes its descriptor on success and refusal.
43
+ // Bounded callers must not allocate an arbitrary directory listing first.
44
+ for await (const entry of await opendir(directory)) {
45
+ if (++visited > options.maximumDirectoryEntries)
46
+ throw new Error("Migration inspection exceeds its bounded directory inventory.");
47
+ if (entry.name.endsWith(".sql"))
48
+ names.push(entry.name);
49
+ }
50
+ }
51
+ names.sort();
52
+ if (options.maximumFiles !== undefined && names.length > options.maximumFiles)
53
+ throw new Error("Migration inspection exceeds its bounded file inventory.");
39
54
  const entries = [];
40
55
  for (const filename of names) {
41
56
  const match = migrationNamePattern.exec(filename);
42
57
  if (!match) {
43
58
  throw new Error(`Invalid migration filename in ${directory}: ${filename}`);
44
59
  }
45
- const bytes = await readFile(new URL(filename, directory));
60
+ const path = new URL(filename, directory);
61
+ const bytes = options.readBytes
62
+ ? await options.readBytes(path, filename)
63
+ : await readFile(path);
46
64
  entries.push({
47
65
  version: match[1],
48
66
  name: match[2],
@@ -0,0 +1,125 @@
1
+ import type { RehearsalConfigPathOptions } from "../project/configuration.mjs";
2
+ import { type MigrationImpactTask, type MigrationImpactUnknownReason } from "./migration_impact_plan.mjs";
3
+ import { type MigrationImpactCount } from "./migration_impact_queries.mjs";
4
+ import { openMigrationImpactRuntime } from "../targets/migration_impact_runtime.mjs";
5
+ type CountUnknownReason = "unsupported_runtime_metadata" | "unsupported_server_version" | "query_failed" | "lock_timeout" | "statement_timeout" | "total_timeout" | "input_limit";
6
+ interface PreparedEntry {
7
+ readonly filename: string;
8
+ readonly sha256: string;
9
+ readonly tasks: readonly MigrationImpactTask[];
10
+ readonly unknownReasons: readonly MigrationImpactUnknownReason[];
11
+ }
12
+ declare const prepare: (options: RehearsalConfigPathOptions, targetName: string | undefined) => Promise<{
13
+ projectRoot: string;
14
+ target: import("./topology.mjs").RuntimeTopologyTarget & {
15
+ summary: ReturnType<(inspection: {
16
+ baselineGenerationId: string;
17
+ baselineCutoff: string;
18
+ candidateSha256: string;
19
+ migrations: {
20
+ version: string;
21
+ name: string;
22
+ filename: string;
23
+ sha256: string;
24
+ status: string;
25
+ }[];
26
+ definitions: {
27
+ represented_by_baseline: string;
28
+ applied_to_current_runtime: string;
29
+ candidate: string;
30
+ modified: string;
31
+ invalid: string;
32
+ };
33
+ }) => Readonly<{
34
+ baselineGenerationId: string;
35
+ candidateSha256: string;
36
+ candidateCount: number;
37
+ candidates: readonly {
38
+ version: string;
39
+ name: string;
40
+ filename: string;
41
+ sha256: string;
42
+ status: string;
43
+ }[];
44
+ }>>;
45
+ };
46
+ baseline: import("../baseline/artifact.mjs").BaselineManifest;
47
+ entries: PreparedEntry[];
48
+ binding: string;
49
+ candidateSha256: string;
50
+ candidateCount: number;
51
+ }>;
52
+ type Prepared = Awaited<ReturnType<typeof prepare>>;
53
+ declare const reportFor: ({ prepared, measured, counts, reason, snapshotAt, runtimeIdentitySha256, contactedDatabase, }: {
54
+ prepared: Prepared;
55
+ measured?: boolean;
56
+ counts?: readonly MigrationImpactCount[];
57
+ reason?: CountUnknownReason;
58
+ snapshotAt?: string | null;
59
+ runtimeIdentitySha256?: string | null;
60
+ contactedDatabase?: boolean;
61
+ }) => {
62
+ schemaVersion: 1;
63
+ inspectorVersion: string;
64
+ candidateSha256: string;
65
+ candidateCount: number;
66
+ target: {
67
+ name: string;
68
+ provider: "postgresql" | "supabase";
69
+ baselineGenerationId: string;
70
+ candidateSha256: string;
71
+ };
72
+ mode: "measurement" | "preview";
73
+ measurementStatus: "measured" | "not_required" | "partial" | "planned" | "unknown";
74
+ snapshotAt: string | null;
75
+ runtimeIdentitySha256: string | null;
76
+ entries: {
77
+ filename: string;
78
+ sha256: string;
79
+ coverage: "incomplete" | "recognized_forms";
80
+ unknownReasons: readonly MigrationImpactUnknownReason[];
81
+ measurements: {
82
+ operation: "broad_delete" | "drop_column" | "drop_table" | "new_constraint" | "not_null" | "truncate";
83
+ metric: "duplicate_rows" | "nulls" | "orphan_rows" | "populated" | "rows";
84
+ line: number;
85
+ column: number;
86
+ evidence: "measured_current_snapshot" | "planned" | "unknown";
87
+ count: string | null;
88
+ unknownReason: CountUnknownReason | null;
89
+ }[];
90
+ }[];
91
+ summary: {
92
+ plannedCount: number;
93
+ measuredCount: number;
94
+ unknownMeasurementCount: number;
95
+ unknownEntryCount: number;
96
+ };
97
+ limits: Readonly<{
98
+ maximumMeasurements: 100;
99
+ lockTimeoutMilliseconds: 1000;
100
+ statementTimeoutMilliseconds: 5000;
101
+ totalTimeoutMilliseconds: 30000;
102
+ maximumOutputBytes: 131072;
103
+ maximumScriptBytes: 262144;
104
+ }>;
105
+ safety: {
106
+ appliesMigrations: boolean;
107
+ startsOrRepairsRuntime: boolean;
108
+ returnsRowContents: boolean;
109
+ returnsSqlOrObjectNames: boolean;
110
+ contactsDatabase: boolean;
111
+ transaction: "repeatable_read_read_only" | null;
112
+ };
113
+ limitation: string;
114
+ };
115
+ export type MigrationImpactReport = ReturnType<typeof reportFor>;
116
+ export declare const inspectMigrationImpact: (options?: RehearsalConfigPathOptions, targetName?: string) => Promise<MigrationImpactReport>;
117
+ export declare const measureMigrationImpact: ({ options, targetName, confirmation, }: {
118
+ options?: RehearsalConfigPathOptions;
119
+ targetName?: string | undefined;
120
+ confirmation?: string | undefined;
121
+ }, implementation?: {
122
+ openRuntime?: typeof openMigrationImpactRuntime;
123
+ }) => Promise<MigrationImpactReport>;
124
+ export declare const renderMigrationImpact: (report: MigrationImpactReport) => string;
125
+ export {};