@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.
- package/CHANGELOG.md +56 -4
- package/COMPATIBILITY.md +41 -4
- package/README.md +8 -4
- package/dist/package.json +3 -1
- package/dist/src/cli/arguments.mjs +1 -0
- package/dist/src/cli/basic_command_handlers.mjs +4 -0
- package/dist/src/cli/editor_command_handlers.mjs +24 -0
- package/dist/src/project/configuration_contract.d.mts +1 -0
- package/dist/src/project/configuration_normalization.mjs +2 -1
- package/dist/src/project/configuration_rendering.mjs +3 -2
- package/dist/src/project/configuration_resolution.mjs +0 -2
- package/dist/src/project/project_detection.d.mts +2 -0
- package/dist/src/project/project_detection.mjs +1 -0
- package/dist/src/project/runtime_project_identity.d.mts +5 -0
- package/dist/src/project/runtime_project_identity.mjs +41 -0
- package/dist/src/project/setup.mjs +3 -1
- package/dist/src/runtime/migration_history.d.mts +5 -1
- package/dist/src/runtime/migration_history.mjs +24 -6
- package/dist/src/runtime/migration_impact.d.mts +125 -0
- package/dist/src/runtime/migration_impact.mjs +278 -0
- package/dist/src/runtime/migration_impact_catalog.d.mts +18 -0
- package/dist/src/runtime/migration_impact_catalog.mjs +116 -0
- package/dist/src/runtime/migration_impact_plan.d.mts +47 -0
- package/dist/src/runtime/migration_impact_plan.mjs +297 -0
- package/dist/src/runtime/migration_impact_queries.d.mts +22 -0
- package/dist/src/runtime/migration_impact_queries.mjs +270 -0
- package/dist/src/runtime/migration_insights.d.mts +137 -0
- package/dist/src/runtime/migration_insights.mjs +143 -0
- package/dist/src/runtime/migration_insights_input.d.mts +13 -0
- package/dist/src/runtime/migration_insights_input.mjs +153 -0
- package/dist/src/runtime/migration_sql_lexer.d.mts +14 -0
- package/dist/src/runtime/migration_sql_lexer.mjs +204 -0
- package/dist/src/runtime/migration_sql_scan.d.mts +16 -0
- package/dist/src/runtime/migration_sql_scan.mjs +369 -0
- package/dist/src/runtime/plan.d.mts +2 -1
- package/dist/src/runtime/plan.mjs +4 -4
- package/dist/src/runtime/policy.d.mts +2 -0
- package/dist/src/runtime/policy.mjs +13 -1
- package/dist/src/runtime/privileges.d.mts +25 -0
- package/dist/src/runtime/privileges.mjs +198 -0
- package/dist/src/runtime/topology.d.mts +1 -1
- package/dist/src/runtime/topology.mjs +2 -2
- package/dist/src/shared/cancellation.mjs +1 -0
- package/dist/src/shared/held_path_ancestry.d.mts +7 -0
- package/dist/src/shared/held_path_ancestry.mjs +110 -0
- package/dist/src/shared/operation_guard.mjs +1 -0
- package/dist/src/targets/migration_impact_runtime.d.mts +22 -0
- package/dist/src/targets/migration_impact_runtime.mjs +367 -0
- package/dist/src/targets/supabase.mjs +1 -0
- package/docs/README.md +20 -7
- package/docs/adapters.md +10 -0
- package/docs/architecture.md +75 -1
- package/docs/commands.md +169 -34
- package/docs/configuration.md +42 -9
- package/docs/getting-started.md +4 -4
- package/docs/index.md +1 -0
- package/docs/releasing.md +15 -11
- package/docs/roadmap.md +49 -9
- package/docs/runtime-lifecycle.md +15 -4
- package/docs/runtime-policies.md +145 -0
- package/docs/stability.md +8 -4
- package/docs/updating.md +73 -0
- package/docs/vscode.md +30 -19
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,60 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable Rehearsal package changes
|
|
4
|
-
a Changelog, and versions
|
|
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
|
-
## [
|
|
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
|
|
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
|
|
30
|
-
matrix
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
},
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
.
|
|
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
|
|
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 {};
|