@rehearsal-db/core 0.1.0-rc.9 → 1.0.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 CHANGED
@@ -5,6 +5,38 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.0.0] - 2026-10-17
9
+
10
+ ### Stable compatibility
11
+
12
+ - Establish the documented CLI, configuration, policy, baseline, TypeScript and
13
+ JSON contracts as the 1.x compatibility boundary. This preserves rc.10's database,
14
+ authentication and restore behavior.
15
+ - Keep the complete MIT CLI free. The separately distributed Free Starter editor,
16
+ future Pro plans and additional database families do not expand the core support
17
+ matrix or change its safety boundaries.
18
+
19
+ ### Fixed
20
+
21
+ - Report unknown CLI commands and options as invalid input (exit 2), rather than
22
+ letting words in the input or help text misclassify them as migration or baseline
23
+ failures. The command remains refused and supported-command help is retained.
24
+
25
+ ## [0.1.0-rc.10] - 2026-10-17
26
+
27
+ ### Fixed
28
+
29
+ - Interactive Ctrl+C and Ctrl+Z retire owned work without leaving an invoking npm
30
+ job suspended. Terminal mode and input consumption are restored after cleanup;
31
+ startup and unanswered migration confirmations cancel safely, and unrelated jobs
32
+ are untouched.
33
+
34
+ ### Documentation
35
+
36
+ - Stable promotion is evidence-based after owner review: complete the release and
37
+ consumer acceptance checks rather than waiting a fixed number of calendar days.
38
+ Exact-artifact, security, data-preservation and human publishing approvals remain.
39
+
8
40
  ## [0.1.0-rc.9] - 2026-10-17
9
41
 
10
42
  ### Fixed
@@ -740,7 +772,14 @@ not unattended cleanup, and container age is not proof of user inactivity.
740
772
  publication uses short-lived trusted OIDC, and every release tag must already exist on
741
773
  protected `main`.
742
774
 
743
- [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.4...HEAD
775
+ [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.0...HEAD
776
+ [1.0.0]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.10...v1.0.0
777
+ [0.1.0-rc.10]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.9...v0.1.0-rc.10
778
+ [0.1.0-rc.9]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.8...v0.1.0-rc.9
779
+ [0.1.0-rc.8]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.7...v0.1.0-rc.8
780
+ [0.1.0-rc.7]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.6...v0.1.0-rc.7
781
+ [0.1.0-rc.6]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.5...v0.1.0-rc.6
782
+ [0.1.0-rc.5]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.4...v0.1.0-rc.5
744
783
  [0.1.0-rc.4]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.3...v0.1.0-rc.4
745
784
  [0.1.0-rc.3]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.2...v0.1.0-rc.3
746
785
  [0.1.0-rc.2]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.1...v0.1.0-rc.2
package/COMPATIBILITY.md CHANGED
@@ -1,10 +1,15 @@
1
1
  # Rehearsal compatibility policy
2
2
 
3
- Rehearsal begins at `0.x`. Minor releases may contain breaking changes, but every
4
- intentional break must be called out in the changelog with a migration path. Patch
5
- releases must remain backward compatible within their minor line.
3
+ Starting with `1.0.0`, documented public contracts are backward compatible throughout
4
+ the `1.x` line. Minor releases may add compatible capabilities; patch releases fix
5
+ defects. An intentional breaking change requires a new major version, a changelog
6
+ entry and a migration path.
6
7
 
7
- The following are compatibility contracts for published 0.x releases:
8
+ Historical `0.x` releases allowed intentional breaking changes in minor versions,
9
+ with changelog and migration guidance. Their patch releases remain compatible within
10
+ their minor line. Existing artifacts and granted license rights are unchanged.
11
+
12
+ The following are compatibility contracts for published releases:
8
13
 
9
14
  - configuration property names, types, defaults, validation, and schema version;
10
15
  - baseline format and checksum meaning;
@@ -17,12 +22,12 @@ A change to any of those requires either a compatible extension or a deliberate
17
22
  transition. Rehearsal must never silently reinterpret an old config, baseline, digest,
18
23
  or successful verification receipt.
19
24
 
20
- The first `1.0.0` release requires external beta evidence, a support policy, and a settled
21
- public API. Supporting additional database families, package managers, or operating
22
- systems is not implied by the 0.x contract.
25
+ The first `1.0.0` release requires external beta evidence, a support policy and the
26
+ settled public API described in [the stability policy](docs/stability.md). Supporting
27
+ additional database families, package managers or operating systems is not implied.
23
28
 
24
29
  Rehearsal supports maintained even-numbered Node.js release lines that pass its CI
25
- matrix. Beta.13 supports Node.js 22, 24, and 26; Node.js 24 LTS is the recommended
30
+ matrix. Rehearsal 1.0 supports Node.js 22, 24, and 26; Node.js 24 LTS is the recommended
26
31
  default and the release-build runtime. End-of-life and odd-numbered Node.js releases are
27
32
  not supported. A future release may remove a Node.js line after its upstream end-of-life
28
33
  date, with that change called out in the changelog.
package/SECURITY.md CHANGED
@@ -9,9 +9,11 @@ Report suspected vulnerabilities through the
9
9
  in this repository. A GitHub account is required. Do not open a
10
10
  public issue containing an exploit, secret, hosted target, or source record.
11
11
 
12
- Supported security updates cover the latest released 0.x minor during beta. A security
13
- fix may deliberately tighten configuration or refuse a workflow that an earlier beta
14
- accepted. During beta, reports apply to the latest published release and current `main`.
12
+ Security updates target the latest released 1.x version. Update to the latest 1.x
13
+ release to receive fixes; older release candidates are not a maintained security line.
14
+ Reports concerning current `main` and prereleases are also welcome. A security fix may
15
+ tighten validation or refuse an unsafe workflow; it must not silently reinterpret
16
+ existing data or claim that a refused operation succeeded.
15
17
 
16
18
  The threat boundary and remaining responsibilities live in
17
19
  [docs/security-model.md](docs/security-model.md). Every
package/SUPPORT.md CHANGED
@@ -3,8 +3,7 @@
3
3
  Use GitHub Issues for reproducible defects and setup problems that include a synthetic
4
4
  reproduction. Include operating system, architecture, Node version, Supabase CLI version,
5
5
  Docker engine, the failing command, safe diagnostics, and the smallest reproduction.
6
- The first beta does not yet maintain a separate public discussion or feature-request
7
- channel.
6
+ Search existing reports before opening a new issue.
8
7
 
9
8
  Run `npx rehearsal support` or choose **Get help** in the guide to collect the environment
10
9
  and readiness portion without assembling it by hand. The report intentionally excludes
@@ -15,6 +14,8 @@ Do not include credentials, connection strings, source rows, baseline artifacts,
15
14
  URLs, or proprietary migrations. Report security problems through private vulnerability
16
15
  reporting as described in [SECURITY.md](SECURITY.md).
17
16
 
18
- The package is pre-release. The supported matrix is defined in
17
+ The supported matrix and 1.x compatibility boundary are defined in
19
18
  [COMPATIBILITY.md](COMPATIBILITY.md); unsupported environments are welcome as clearly
20
19
  labelled experiments but cannot be treated as verified behavior.
20
+
21
+ Support is community-based; no response-time guarantee or production SLA is offered.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehearsal-db/core",
3
- "version": "0.1.0-rc.9",
3
+ "version": "1.0.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": {
@@ -76,7 +76,7 @@
76
76
  "test:fixture:standalone": "npm run build && node scripts/verification/run_scenario.mjs standalone",
77
77
  "test:fixture:privacy": "npm run build && node scripts/verification/run_scenario.mjs privacy",
78
78
  "test:fixture:resources": "npm run build && node scripts/verification/run_scenario.mjs resources",
79
- "release:verify": "node scripts/verification/release_gate.mjs",
79
+ "release:verify": "npm run build && node scripts/verification/release_gate.mjs",
80
80
  "check": "npm run check:syntax && npm run typecheck && npm run test && npm run docs:build && npm run format:check && npm run package:audit",
81
81
  "check:syntax": "find scripts tests -type f -name '*.mjs' -exec node --check {} +",
82
82
  "typecheck": "npm run build && tsc --project tsconfig.json",
@@ -1,12 +1,8 @@
1
1
  /** Run identity discovery while an owned local application remains available. */
2
2
  import type { EventEmitter } from "node:events";
3
+ import { type TerminalInput } from "../shared/terminal_input.mjs";
3
4
  import { type ApplicationDiagnostics, type ApplicationSession, type ApplicationSessionSignal } from "./session.mjs";
4
5
  type SignalTarget = Pick<EventEmitter, "prependListener" | "removeListener">;
5
- interface SessionInput {
6
- readonly isTTY?: boolean;
7
- prependListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
8
- removeListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
9
- }
10
6
  /**
11
7
  * A terminal interruption aborts the task and retires the owned process group.
12
8
  * A completed task leaves the session running so its caller can deliberately
@@ -16,7 +12,7 @@ export declare const runApplicationSessionTask: <Value>({ session, task, signalT
16
12
  session: ApplicationSession;
17
13
  task: (signal: AbortSignal) => Promise<Value>;
18
14
  signalTarget?: SignalTarget;
19
- input?: SessionInput;
15
+ input?: TerminalInput | null;
20
16
  onReady?: (ready: ApplicationSession["ready"]) => void;
21
17
  }) => Promise<Readonly<{
22
18
  completed: true;
@@ -1,4 +1,5 @@
1
1
  /** Run identity discovery while an owned local application remains available. */
2
+ import { acquireTerminalInput, } from "../shared/terminal_input.mjs";
2
3
  import { summarizeProjectCommandFailure, } from "./session.mjs";
3
4
  const SESSION_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGTSTP"];
4
5
  /**
@@ -23,19 +24,10 @@ export const runApplicationSessionTask = async ({ session, task, signalTarget =
23
24
  for (const [signal, handler] of handlers) {
24
25
  signalTarget.prependListener(signal, handler);
25
26
  }
26
- const onInput = (chunk) => {
27
- const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
28
- if (bytes.includes(0x03)) {
29
- finishInterruption({ kind: "interrupted", signal: "SIGINT" });
30
- }
31
- else if (bytes.includes(0x1a)) {
32
- finishInterruption({ kind: "interrupted", signal: "SIGTSTP" });
33
- }
34
- };
35
- if (input?.isTTY)
36
- input.prependListener("data", onInput);
27
+ let releaseInput = () => undefined;
37
28
  const abortController = new AbortController();
38
29
  try {
30
+ releaseInput = acquireTerminalInput(input, (signal) => finishInterruption({ kind: "interrupted", signal }));
39
31
  onReady(session.ready);
40
32
  const outcome = await Promise.race([
41
33
  interruption,
@@ -77,7 +69,6 @@ export const runApplicationSessionTask = async ({ session, task, signalTarget =
77
69
  for (const [signal, handler] of handlers) {
78
70
  signalTarget.removeListener(signal, handler);
79
71
  }
80
- if (input?.isTTY)
81
- input.removeListener("data", onInput);
72
+ releaseInput();
82
73
  }
83
74
  };
@@ -3,7 +3,8 @@ import { spawn } from "node:child_process";
3
3
  import type { EventEmitter } from "node:events";
4
4
  import { waitForApplicationIdle } from "./activity_monitor.mjs";
5
5
  import type { ApplicationActivity } from "./activity_contract.mjs";
6
- declare const SESSION_SIGNALS: string[];
6
+ import { type TerminalInput } from "../shared/terminal_input.mjs";
7
+ declare const SESSION_SIGNALS: readonly ["SIGINT", "SIGTERM", "SIGHUP", "SIGTSTP"];
7
8
  type SessionSignal = (typeof SESSION_SIGNALS)[number];
8
9
  export type ApplicationSessionSignal = SessionSignal;
9
10
  export interface ApplicationReadiness {
@@ -29,11 +30,6 @@ interface ChildExit {
29
30
  readonly status: number | null;
30
31
  readonly signal: NodeJS.Signals | null;
31
32
  }
32
- interface SessionInput {
33
- readonly isTTY?: boolean;
34
- prependListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
35
- removeListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
36
- }
37
33
  interface EnvironmentInput {
38
34
  readonly files: Readonly<Record<string, string>> & {
39
35
  readonly primary: string;
@@ -66,7 +62,7 @@ export declare const summarizeProjectCommandFailure: ({ error, status, signal, s
66
62
  stdoutByteCount?: number;
67
63
  stderrByteCount?: number;
68
64
  }) => string;
69
- export declare const startApplicationSession: ({ command, cwd, files, mappings, readiness, inheritedEnvironment, spawnImplementation, fetchImplementation, signalTarget, portPreflight, }: EnvironmentInput & {
65
+ export declare const startApplicationSession: ({ command, cwd, files, mappings, readiness, inheritedEnvironment, spawnImplementation, fetchImplementation, signalTarget, input, portPreflight, }: EnvironmentInput & {
70
66
  command: string;
71
67
  cwd: string;
72
68
  readiness: ApplicationReadiness;
@@ -74,6 +70,7 @@ export declare const startApplicationSession: ({ command, cwd, files, mappings,
74
70
  spawnImplementation?: typeof spawn;
75
71
  fetchImplementation?: typeof fetch;
76
72
  signalTarget?: SignalTarget;
73
+ input?: TerminalInput | null;
77
74
  portPreflight?: (readiness: ApplicationReadiness) => Promise<void>;
78
75
  }) => Promise<ApplicationSession>;
79
76
  /**
@@ -83,7 +80,7 @@ export declare const startApplicationSession: ({ command, cwd, files, mappings,
83
80
  export declare const holdApplicationSession: ({ session, signalTarget, input, onReady, activity, idleTimeoutSeconds, idleMonitorImplementation, onActivityAvailability, }: {
84
81
  session: ApplicationSession;
85
82
  signalTarget?: SignalTarget;
86
- input?: SessionInput;
83
+ input?: TerminalInput | null;
87
84
  onReady?: (ready: ApplicationSession["ready"]) => void;
88
85
  activity?: ApplicationActivity | null;
89
86
  idleTimeoutSeconds?: number | null;
@@ -7,6 +7,8 @@ import { createCleanProcessEnvironment, isLoopbackUrl, } from "../shared/process
7
7
  import { assertApplicationPortAvailable } from "./port_preflight.mjs";
8
8
  import { waitForApplicationIdle } from "./activity_monitor.mjs";
9
9
  import { stopOwnedPosixProcessGroup } from "./process_group.mjs";
10
+ import { acquireTerminalInput, } from "../shared/terminal_input.mjs";
11
+ import { createOperationCancelledError, throwIfOperationCancelled, } from "../shared/cancellation.mjs";
10
12
  const ENV_KEY = /^[A-Z][A-Z0-9_]*$/u;
11
13
  const MAX_DIAGNOSTIC_BYTES = 64 * 1024;
12
14
  const SESSION_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGTSTP"];
@@ -188,7 +190,7 @@ const stopOwnedChild = async (child) => {
188
190
  throw new Error("Owned application process group did not stop within its shutdown bound. No unrelated process was targeted.");
189
191
  }
190
192
  };
191
- export const startApplicationSession = async ({ command, cwd, files, mappings, readiness, inheritedEnvironment = process.env, spawnImplementation = spawn, fetchImplementation = fetch, signalTarget = process, portPreflight = (value) => assertApplicationPortAvailable({ url: value?.url }), }) => {
193
+ export const startApplicationSession = async ({ command, cwd, files, mappings, readiness, inheritedEnvironment = process.env, spawnImplementation = spawn, fetchImplementation = fetch, signalTarget = process, input = process.stdin, portPreflight = (value) => assertApplicationPortAvailable({ url: value?.url }), }) => {
192
194
  const mapped = await buildApplicationEnvironment({ files, mappings });
193
195
  const [program, ...args] = parseCommand(command);
194
196
  await portPreflight(readiness);
@@ -228,13 +230,22 @@ export const startApplicationSession = async ({ command, cwd, files, mappings, r
228
230
  });
229
231
  let interruptStartup = () => undefined;
230
232
  const interruption = new Promise((_, reject) => {
231
- interruptStartup = (signal) => reject(new Error(`Application startup was interrupted by ${signal}.`));
233
+ interruptStartup = (signal) => reject(createOperationCancelledError(signal));
232
234
  });
233
- const startupHandlers = new Map(SESSION_SIGNALS.map((signal) => [signal, () => interruptStartup(signal)]));
235
+ let releaseInput = null;
236
+ const startupHandlers = new Map(SESSION_SIGNALS.map((signal) => [
237
+ signal,
238
+ () => {
239
+ if (!releaseInput || releaseInput.isCurrentOwner())
240
+ interruptStartup(signal);
241
+ },
242
+ ]));
234
243
  for (const [signal, handler] of startupHandlers) {
235
244
  signalTarget.prependListener(signal, handler);
236
245
  }
237
246
  try {
247
+ releaseInput = acquireTerminalInput(input, interruptStartup);
248
+ throwIfOperationCancelled();
238
249
  const ready = await Promise.race([
239
250
  waitForReadiness({
240
251
  readiness,
@@ -255,13 +266,19 @@ export const startApplicationSession = async ({ command, cwd, files, mappings, r
255
266
  });
256
267
  }
257
268
  catch (error) {
258
- await stopApplication();
269
+ try {
270
+ await stopApplication();
271
+ }
272
+ catch (cleanupError) {
273
+ throw new AggregateError([error, cleanupError], "Application startup failed and its owned process could not be stopped.");
274
+ }
259
275
  throw error;
260
276
  }
261
277
  finally {
262
278
  for (const [signal, handler] of startupHandlers) {
263
279
  signalTarget.removeListener(signal, handler);
264
280
  }
281
+ releaseInput?.();
265
282
  }
266
283
  };
267
284
  /**
@@ -278,26 +295,23 @@ export const holdApplicationSession = async ({ session, signalTarget = process,
278
295
  const interruption = new Promise((resolve) => {
279
296
  finishInterruption = resolve;
280
297
  });
298
+ let releaseInput = null;
281
299
  const handlers = new Map(SESSION_SIGNALS.map((signal) => [
282
300
  signal,
283
- () => finishInterruption({ kind: "interrupted", signal }),
301
+ () => {
302
+ if (!releaseInput || releaseInput.isCurrentOwner())
303
+ finishInterruption({ kind: "interrupted", signal });
304
+ },
284
305
  ]));
285
306
  for (const [signal, handler] of handlers) {
286
307
  signalTarget.prependListener(signal, handler);
287
308
  }
288
- const onInput = (chunk) => {
289
- const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
290
- if (bytes.includes(0x03)) {
291
- finishInterruption({ kind: "interrupted", signal: "SIGINT" });
292
- }
293
- else if (bytes.includes(0x1a)) {
294
- finishInterruption({ kind: "interrupted", signal: "SIGTSTP" });
295
- }
296
- };
297
- if (input?.isTTY)
298
- input.prependListener("data", onInput);
299
309
  const activityController = new AbortController();
310
+ let operationFailed = false;
311
+ let operationError;
300
312
  try {
313
+ releaseInput = acquireTerminalInput(input, (signal) => finishInterruption({ kind: "interrupted", signal }));
314
+ throwIfOperationCancelled();
301
315
  if (idleTimeoutSeconds !== null &&
302
316
  (!activity ||
303
317
  !Number.isSafeInteger(idleTimeoutSeconds) ||
@@ -338,20 +352,31 @@ export const holdApplicationSession = async ({ session, signalTarget = process,
338
352
  },
339
353
  });
340
354
  }
355
+ catch (error) {
356
+ operationFailed = true;
357
+ operationError = error;
358
+ throw error;
359
+ }
341
360
  finally {
342
361
  activityController.abort();
343
362
  try {
344
363
  // npm can forward the terminal's SIGINT after the CLI receives it.
345
364
  // Keep our handlers installed until the owned process group is fully
346
365
  // stopped so a duplicate signal cannot terminate Rehearsal mid-cleanup.
347
- await session.stop();
366
+ try {
367
+ await session.stop();
368
+ }
369
+ catch (cleanupError) {
370
+ if (operationFailed)
371
+ throw new AggregateError([operationError, cleanupError], "Application session failed and its owned process could not be stopped.");
372
+ throw cleanupError;
373
+ }
348
374
  }
349
375
  finally {
350
376
  for (const [signal, handler] of handlers) {
351
377
  signalTarget.removeListener(signal, handler);
352
378
  }
353
- if (input?.isTTY)
354
- input.removeListener("data", onInput);
379
+ releaseInput?.();
355
380
  }
356
381
  }
357
382
  };
@@ -1,4 +1,5 @@
1
1
  /** Parse and classify the stable Rehearsal command-line contract. */
2
+ import { RehearsalError } from "../shared/diagnostics.mjs";
2
3
  export const parseArguments = (arguments_) => {
3
4
  const flags = {
4
5
  json: false,
@@ -86,7 +87,13 @@ export const parseArguments = (arguments_) => {
86
87
  else if (argument.startsWith("--image-id="))
87
88
  (flags.imageIds ??= []).push(argument.slice("--image-id=".length));
88
89
  else if (argument.startsWith("--"))
89
- throw new Error(`Unknown Rehearsal option: ${argument}.`);
90
+ throw new RehearsalError({
91
+ category: "configuration_invalid",
92
+ code: "CONFIGURATION_INVALID",
93
+ message: `Unknown Rehearsal option: ${argument}.`,
94
+ refused: "No Rehearsal command was executed.",
95
+ suggestions: ["Run rehearsal --help to list supported options."],
96
+ });
90
97
  else
91
98
  positionals.push(argument);
92
99
  }
@@ -8,7 +8,7 @@ import { join } from "node:path";
8
8
  import { performance } from "node:perf_hooks";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import * as prompts from "@clack/prompts";
11
- import { normalizeRehearsalError, renderHumanError, serializeRehearsalError, } from "../shared/diagnostics.mjs";
11
+ import { RehearsalError, normalizeRehearsalError, renderHumanError, serializeRehearsalError, } from "../shared/diagnostics.mjs";
12
12
  import { acquireProjectOperation, assertInstalledPackageFingerprint, createInstalledPackageFingerprint, } from "../shared/operation_guard.mjs";
13
13
  import { commandMutatesProject, parseArguments, resetGuidedFlags, } from "./arguments.mjs";
14
14
  import { usage } from "./basic_command_handlers.mjs";
@@ -70,7 +70,13 @@ const executeCommand = async ({ command, flags, planOptions, guided, }) => {
70
70
  state,
71
71
  });
72
72
  if (!result) {
73
- throw new Error(`Unknown Rehearsal command: ${command}.\n\n${usage()}`);
73
+ throw new RehearsalError({
74
+ category: "configuration_invalid",
75
+ code: "CONFIGURATION_INVALID",
76
+ message: `Unknown Rehearsal command: ${command}.\n\n${usage()}`,
77
+ refused: "No Rehearsal command was executed.",
78
+ suggestions: ["Run rehearsal --help to list supported commands."],
79
+ });
74
80
  }
75
81
  const exitCode = emitCommand({ command, flags, result });
76
82
  if (exitCode !== undefined)
@@ -141,12 +147,18 @@ const main = async () => {
141
147
  removeGuidedExitShortcut = null;
142
148
  }
143
149
  try {
144
- await executeCommand({
150
+ const executeSelected = () => executeCommand({
145
151
  command: selected,
146
152
  flags,
147
153
  planOptions,
148
154
  guided: true,
149
155
  });
156
+ if (opensApplication) {
157
+ await runWithOperationCancellation({ task: executeSelected });
158
+ }
159
+ else {
160
+ await executeSelected();
161
+ }
150
162
  }
151
163
  finally {
152
164
  if (opensApplication) {
@@ -473,6 +473,7 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
473
473
  };
474
474
  const startRuntimeApplication = async ({ flags, planOptions, session: commandSession, onRuntimeStarted = () => undefined, }) => {
475
475
  const topology = await loadRuntimeTopology(planOptions);
476
+ throwIfOperationCancelled();
476
477
  const application = topology.primary.config.application;
477
478
  const readiness = application.readiness;
478
479
  if (!readiness) {
@@ -516,6 +517,18 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
516
517
  mappings: application.environmentVariables,
517
518
  readiness,
518
519
  }));
520
+ try {
521
+ throwIfOperationCancelled();
522
+ }
523
+ catch (error) {
524
+ try {
525
+ await applicationSession.stop();
526
+ }
527
+ catch (cleanupError) {
528
+ throw new AggregateError([error, cleanupError], "Application handoff was cancelled and its owned process could not be stopped.");
529
+ }
530
+ throw error;
531
+ }
519
532
  return {
520
533
  started: started.runtime,
521
534
  verified: verified.runtime,
@@ -527,6 +540,7 @@ export const createRuntimeCommands = ({ packageRoot, projectRoot, managerPath, g
527
540
  };
528
541
  const openRuntimeApplication = async ({ flags, planOptions, session: commandSession, onReady = () => undefined, }) => {
529
542
  const topology = await loadRuntimeTopology(planOptions);
543
+ throwIfOperationCancelled();
530
544
  let runtimeStarted = false;
531
545
  const { result, lifecycle } = await applyRuntimePersistence({
532
546
  mode: topology.primary.config.lifecycle.open,
@@ -1,6 +1,8 @@
1
1
  /** Terminal presentation and prompt primitives shared by guided CLI workflows. */
2
2
  import { createInterface } from "node:readline/promises";
3
3
  import * as prompts from "@clack/prompts";
4
+ import { hasTerminalInputLease, reassertTerminalInputLease, } from "../shared/terminal_input.mjs";
5
+ import { getOperationCancellationSignal } from "../shared/cancellation.mjs";
4
6
  const useColor = (flags) => !flags.plain &&
5
7
  process.stdout.isTTY &&
6
8
  process.env.NO_COLOR === undefined &&
@@ -54,22 +56,39 @@ export const promptForChoice = async ({ message, options, flags = {}, optionsAlr
54
56
  }
55
57
  };
56
58
  export async function promptForConfirmation(message, flags = {}, { cancelValue = false } = {}) {
59
+ const signal = getOperationCancellationSignal();
60
+ if (signal?.aborted)
61
+ throw signal.reason;
57
62
  if (useStyledPrompts(flags)) {
58
- const accepted = await prompts.confirm({ message, initialValue: false });
59
- return prompts.isCancel(accepted) ? cancelValue : accepted;
63
+ try {
64
+ const accepted = await prompts.confirm({
65
+ message,
66
+ initialValue: false,
67
+ ...(signal ? { signal } : {}),
68
+ });
69
+ return prompts.isCancel(accepted) ? cancelValue : accepted;
70
+ }
71
+ finally {
72
+ reassertTerminalInputLease(process.stdin);
73
+ }
60
74
  }
61
75
  const prompt = createInterface({
62
76
  input: process.stdin,
63
77
  output: process.stdout,
64
78
  });
65
79
  try {
66
- const answer = (await prompt.question(`${message} (y/N) `))
80
+ const answer = (await prompt.question(`${message} (y/N) `, { signal }))
67
81
  .trim()
68
82
  .toLowerCase();
69
83
  return answer === "y" || answer === "yes";
70
84
  }
71
85
  finally {
72
- prompt.close();
86
+ try {
87
+ prompt.close();
88
+ }
89
+ finally {
90
+ reassertTerminalInputLease(process.stdin);
91
+ }
73
92
  }
74
93
  }
75
94
  export const promptForPath = async ({ message, defaultValue, optional = false, flags = {}, validate, }) => {
@@ -203,7 +222,7 @@ export const promptForMultipleChoice = async ({ message, options, initialValues
203
222
  export const installGuidedExitShortcut = (flags, { beforeExit } = {}) => {
204
223
  let exiting = false;
205
224
  const exit = () => {
206
- if (exiting)
225
+ if (exiting || hasTerminalInputLease(process.stdin))
207
226
  return;
208
227
  exiting = true;
209
228
  if (process.stdin.isTTY && process.stdin.isRaw)
@@ -1,5 +1,5 @@
1
1
  /** Explicit, non-destructive persistence policy for project-owned runtimes. */
2
- import { withOperationCancellationShield } from "../shared/cancellation.mjs";
2
+ import { getOperationCancellationSignal, withOperationCancellationShield, } from "../shared/cancellation.mjs";
3
3
  export const shouldStopRuntime = (mode) => mode === "stop-after-run" || mode === "stop-on-application-exit";
4
4
  export const applyRuntimePersistence = async ({ mode, operation, stop, canStop = () => true, }) => {
5
5
  let result;
@@ -20,8 +20,11 @@ export const applyRuntimePersistence = async ({ mode, operation, stop, canStop =
20
20
  stopped = true;
21
21
  }
22
22
  catch (stopError) {
23
- if (operationFailed) {
24
- throw new AggregateError([operationError, stopError], "The Rehearsal operation failed and its owned runtime could not be stopped automatically.");
23
+ // The shield has unwound here. A later interruption must not let the
24
+ // outer cancellation owner replace a real cleanup fault with clean130.
25
+ const cancellation = getOperationCancellationSignal();
26
+ if (operationFailed || cancellation?.aborted) {
27
+ throw new AggregateError([operationFailed ? operationError : cancellation.reason, stopError], "The Rehearsal operation failed and its owned runtime could not be stopped automatically.");
25
28
  }
26
29
  throw stopError;
27
30
  }
@@ -1,6 +1,7 @@
1
1
  /** Cooperative cancellation state for one mutating CLI operation. */
2
2
  import type { EventEmitter } from "node:events";
3
3
  import { RehearsalError } from "./diagnostics.mjs";
4
+ import { type TerminalInput } from "./terminal_input.mjs";
4
5
  export declare const OPERATION_CANCELLATION_SIGNALS: readonly ["SIGINT", "SIGTERM", "SIGHUP", "SIGTSTP"];
5
6
  export type OperationCancellationSignal = (typeof OPERATION_CANCELLATION_SIGNALS)[number];
6
7
  type SignalTarget = Pick<EventEmitter, "prependListener" | "removeListener">;
@@ -11,8 +12,9 @@ export declare const getOperationCancellationSignal: () => AbortSignal | undefin
11
12
  export declare const throwIfOperationCancelled: () => void;
12
13
  export declare const reachOperationCancellationCheckpoint: () => Promise<void>;
13
14
  export declare const withOperationCancellationShield: <Value>(task: () => Promise<Value>) => Promise<Value>;
14
- export declare const runWithOperationCancellation: <Value>({ task, signalTarget, }: {
15
+ export declare const runWithOperationCancellation: <Value>({ task, signalTarget, input, }: {
15
16
  task: (signal: AbortSignal) => Promise<Value>;
16
17
  signalTarget?: SignalTarget;
18
+ input?: TerminalInput | null;
17
19
  }) => Promise<Value>;
18
20
  export {};
@@ -1,5 +1,6 @@
1
1
  /** Cooperative cancellation state for one mutating CLI operation. */
2
2
  import { RehearsalError } from "./diagnostics.mjs";
3
+ import { acquireTerminalInput, } from "./terminal_input.mjs";
3
4
  export const OPERATION_CANCELLATION_SIGNALS = [
4
5
  "SIGINT",
5
6
  "SIGTERM",
@@ -8,6 +9,7 @@ export const OPERATION_CANCELLATION_SIGNALS = [
8
9
  ];
9
10
  const CANCELLABLE_COMMANDS = new Set([
10
11
  "migrate",
12
+ "open",
11
13
  "refresh",
12
14
  "reset",
13
15
  "run",
@@ -48,24 +50,29 @@ export const withOperationCancellationShield = async (task) => {
48
50
  shieldDepth -= 1;
49
51
  }
50
52
  };
51
- export const runWithOperationCancellation = async ({ task, signalTarget = process, }) => {
53
+ export const runWithOperationCancellation = async ({ task, signalTarget = process, input = process.stdin, }) => {
52
54
  if (activeSignal) {
53
55
  throw new Error("A cancellable Rehearsal operation is already active.");
54
56
  }
55
57
  const controller = new AbortController();
56
58
  activeSignal = controller.signal;
59
+ let releaseInput = null;
60
+ const interrupt = (signal) => {
61
+ if (!controller.signal.aborted)
62
+ controller.abort(createOperationCancelledError(signal));
63
+ };
57
64
  const handlers = new Map(OPERATION_CANCELLATION_SIGNALS.map((signal) => [
58
65
  signal,
59
66
  () => {
60
- if (!controller.signal.aborted) {
61
- controller.abort(createOperationCancelledError(signal));
62
- }
67
+ if (!releaseInput || releaseInput.isCurrentOwner())
68
+ interrupt(signal);
63
69
  },
64
70
  ]));
65
71
  for (const [signal, handler] of handlers) {
66
72
  signalTarget.prependListener(signal, handler);
67
73
  }
68
74
  try {
75
+ releaseInput = acquireTerminalInput(input, interrupt);
69
76
  const result = await task(controller.signal);
70
77
  throwIfOperationCancelled();
71
78
  return result;
@@ -82,5 +89,6 @@ export const runWithOperationCancellation = async ({ task, signalTarget = proces
82
89
  }
83
90
  activeSignal = undefined;
84
91
  shieldDepth = 0;
92
+ releaseInput?.();
85
93
  }
86
94
  };
@@ -0,0 +1,25 @@
1
+ /** Own terminal control bytes without suspending the invoking shell's npm job. */
2
+ export interface TerminalInput {
3
+ readonly isTTY?: boolean;
4
+ readonly isRaw?: boolean;
5
+ readonly readableFlowing?: boolean | null;
6
+ setRawMode?(enabled: boolean): unknown;
7
+ resume?(): unknown;
8
+ pause?(): unknown;
9
+ prependListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
10
+ removeListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
11
+ }
12
+ export interface TerminalInputLease {
13
+ (): void;
14
+ isCurrentOwner(): boolean;
15
+ }
16
+ export declare const hasTerminalInputLease: (input: TerminalInput | null | undefined) => boolean;
17
+ /** Prompt libraries may restore cooked mode; resume only a still-owned lease. */
18
+ export declare const reassertTerminalInputLease: (input: TerminalInput | null | undefined) => void;
19
+ /**
20
+ * Raw input turns Ctrl+C/Ctrl+Z into bytes instead of kernel signals to the
21
+ * entire foreground npm/shell job. Signal handlers remain responsible for
22
+ * externally delivered signals. The final overlapping lease restores only the
23
+ * mode/flow state this owner acquired; a pre-existing raw terminal stays raw.
24
+ */
25
+ export declare const acquireTerminalInput: (input: TerminalInput | null | undefined, onInterrupt: (signal: "SIGINT" | "SIGTSTP") => void) => TerminalInputLease;
@@ -0,0 +1,129 @@
1
+ /** Own terminal control bytes without suspending the invoking shell's npm job. */
2
+ const leases = new WeakMap();
3
+ export const hasTerminalInputLease = (input) => !!input && leases.has(input);
4
+ /** Prompt libraries may restore cooked mode; resume only a still-owned lease. */
5
+ export const reassertTerminalInputLease = (input) => {
6
+ if (!input || !leases.get(input)?.interactive)
7
+ return;
8
+ if (input.isRaw !== true)
9
+ input.setRawMode?.(true);
10
+ input.resume?.();
11
+ };
12
+ const restoreInput = (input, lease) => {
13
+ if (!lease.interactive)
14
+ return;
15
+ const errors = [];
16
+ try {
17
+ if (lease.changedRaw)
18
+ input.setRawMode?.(lease.wasRaw);
19
+ }
20
+ catch (error) {
21
+ errors.push(error);
22
+ }
23
+ try {
24
+ // A previously untouched (null) stream must return to non-consuming mode,
25
+ // just like a previously paused stream; do not leave stdin keeping Node alive.
26
+ if (lease.wasFlowing)
27
+ input.resume?.();
28
+ else
29
+ input.pause?.();
30
+ }
31
+ catch (error) {
32
+ errors.push(error);
33
+ }
34
+ if (errors.length === 1)
35
+ throw errors[0];
36
+ if (errors.length > 1)
37
+ throw new AggregateError(errors, "Terminal input restoration failed.");
38
+ };
39
+ /**
40
+ * Raw input turns Ctrl+C/Ctrl+Z into bytes instead of kernel signals to the
41
+ * entire foreground npm/shell job. Signal handlers remain responsible for
42
+ * externally delivered signals. The final overlapping lease restores only the
43
+ * mode/flow state this owner acquired; a pre-existing raw terminal stays raw.
44
+ */
45
+ export const acquireTerminalInput = (input, onInterrupt) => {
46
+ if (!input)
47
+ return Object.assign(() => undefined, { isCurrentOwner: () => true });
48
+ const owner = { interrupt: onInterrupt };
49
+ let lease = leases.get(input);
50
+ const first = !lease;
51
+ if (!lease) {
52
+ const owners = [];
53
+ lease = {
54
+ owners,
55
+ interactive: input.isTTY === true,
56
+ wasRaw: input.isRaw === true,
57
+ wasFlowing: input.readableFlowing === true,
58
+ changedRaw: input.isTTY === true &&
59
+ input.isRaw !== true &&
60
+ typeof input.setRawMode === "function",
61
+ dispatch: (chunk) => {
62
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
63
+ const signal = bytes.includes(0x03)
64
+ ? "SIGINT"
65
+ : bytes.includes(0x1a)
66
+ ? "SIGTSTP"
67
+ : null;
68
+ // Capture one owner before dispatch: synchronous release must not deliver
69
+ // the same byte again to an outer operation.
70
+ const current = owners.at(-1);
71
+ if (signal)
72
+ current?.interrupt(signal);
73
+ },
74
+ };
75
+ leases.set(input, lease);
76
+ }
77
+ lease.owners.push(owner);
78
+ try {
79
+ if (first && lease.changedRaw)
80
+ input.setRawMode(true);
81
+ if (first && lease.interactive)
82
+ input.prependListener("data", lease.dispatch);
83
+ if (lease.interactive)
84
+ input.resume?.();
85
+ }
86
+ catch (error) {
87
+ const errors = [error];
88
+ lease.owners.splice(lease.owners.indexOf(owner), 1);
89
+ try {
90
+ if (first && lease.interactive)
91
+ input.removeListener("data", lease.dispatch);
92
+ }
93
+ catch (cleanupError) {
94
+ errors.push(cleanupError);
95
+ }
96
+ if (first) {
97
+ leases.delete(input);
98
+ try {
99
+ restoreInput(input, lease);
100
+ }
101
+ catch (cleanupError) {
102
+ errors.push(cleanupError);
103
+ }
104
+ }
105
+ if (errors.length > 1)
106
+ throw new AggregateError(errors, "Terminal input could not be acquired or restored.");
107
+ throw error;
108
+ }
109
+ let released = false;
110
+ const release = () => {
111
+ if (released)
112
+ return;
113
+ released = true;
114
+ lease.owners.splice(lease.owners.indexOf(owner), 1);
115
+ if (lease.owners.length === 0) {
116
+ leases.delete(input);
117
+ try {
118
+ if (lease.interactive)
119
+ input.removeListener("data", lease.dispatch);
120
+ }
121
+ finally {
122
+ restoreInput(input, lease);
123
+ }
124
+ }
125
+ };
126
+ return Object.assign(release, {
127
+ isCurrentOwner: () => !released && lease.owners.at(-1) === owner,
128
+ });
129
+ };
package/docs/commands.md CHANGED
@@ -324,7 +324,8 @@ Clients should disable conflicting actions unless the state is `idle`.
324
324
 
325
325
  ### Cancelling a mutation
326
326
 
327
- `run`, `reset`, `migrate`, and confirmed `refresh` support package-owned cancellation.
327
+ `run`, `reset`, `migrate`, confirmed `refresh`, and direct or guided `open` preparation support
328
+ package-owned cancellation.
328
329
  Send `SIGINT`, `SIGTERM`, `SIGHUP`, or `SIGTSTP` to the Rehearsal CLI process. Rehearsal
329
330
  handles repeated delivery as one request, forwards it only to the detached process group
330
331
  it launched, waits for package rollback and child cleanup, releases the project lock, and
@@ -343,6 +344,14 @@ The CLI does not stop Docker itself, signal unrelated processes, prune shared re
343
344
  or delete unrelated volumes. `SIGTSTP` is treated as cancellation and becomes `SIGTERM`
344
345
  for the owned child group instead of suspending it mid-cleanup. Windows uses direct child
345
346
  signalling; the detached process-group guarantee is supported on macOS and Linux.
347
+ During an interactive cancellable operation, Rehearsal temporarily owns terminal control
348
+ input so `Ctrl+C` and `Ctrl+Z` request cleanup without suspending an invoking npm job.
349
+ It restores the terminal's previous raw mode and consuming/non-consuming input state
350
+ after cleanup. Piped input is not consumed by this shortcut.
351
+ For direct or guided `open`, cancellation before the ready attached session returns this same
352
+ exit `130` contract, including interruption during runtime preparation or application
353
+ readiness. A cancellation during the ready application's handoff stops that owned
354
+ application before returning; cleanup failure is not reported as clean cancellation.
346
355
 
347
356
  `SIGKILL`, host shutdown, and a force-quit cannot produce a result or run cleanup. After
348
357
  one of those events, clients must not guess: run `activity --json`. A `stale` or `invalid`
@@ -384,6 +393,13 @@ default `keep-until-stop` leaves databases running; `stop-on-application-exit` s
384
393
  with database and Storage changes preserved. Likewise, `lifecycle.run` can remain
385
394
  `keep-until-stop` or use `stop-after-run` for bounded automation. JSON results include
386
395
  the selected mode, final runtime state, and preserved-data guarantee.
396
+ While the ready application or copied-account sign-in session is attached, terminal
397
+ `Ctrl+C` and `Ctrl+Z` are handled as control bytes rather than suspending npm wrappers.
398
+ Rehearsal restores the prior terminal mode after retiring its owned application; it
399
+ never resumes or signals an invoking shell, npm ancestor, or unrelated job.
400
+ Once the ready application is attached, its handled `Ctrl+C`, `Ctrl+Z`, `SIGINT`, or
401
+ `SIGTSTP` closes the session with exit `0` as before; it does not also cancel an outer
402
+ preparation owner. Nested input ownership dispatches a control byte only once.
387
403
 
388
404
  ## Clean up disk space
389
405
 
package/docs/releasing.md CHANGED
@@ -154,6 +154,28 @@ The independent Rehearsal Test Lab is the primary pre-release consumer environme
154
154
  Real product integrations provide additional compatibility evidence; they do not
155
155
  replace the package-owned release gate or Test Lab.
156
156
 
157
+ ### Optional remote verification
158
+
159
+ When local resource guards refuse a heavy run, the existing full release check can
160
+ run on an isolated GitHub-hosted runner instead:
161
+
162
+ ```bash
163
+ gh workflow run verify.yml --ref YOUR_REVIEWED_RELEASE_BRANCH -f exact_release=true
164
+ ```
165
+
166
+ This manual-only option runs `npm run release:verify` and the production dependency
167
+ audit. It adds no required branch check and has no publishing permission or npm
168
+ environment. It does not bypass a local resource guard or start local services.
169
+ The other existing verification jobs still run normally.
170
+
171
+ Download the run's `exact-release-<commit>-<attempt>` artifact within seven days.
172
+ It contains only the frozen tarball and release reports. Require a successful job
173
+ and all nine passed stages in `acceptance.json`, match the reviewed source commit
174
+ and tarball SHA-256, then use those exact bytes for consumer checks and publication
175
+ authorization. Reports retained after failure are failure evidence, not approval.
176
+ This is an execution option for the same release requirements, not a substitute for
177
+ consumer acceptance, supported-platform observations or human publication approval.
178
+
157
179
  The workflow publishes beta and release-candidate versions under the `beta` dist-tag.
158
180
  While Rehearsal has no stable release, it also moves `latest` to the same reviewed
159
181
  prerelease. This keeps the npm package page and the ordinary
package/docs/stability.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Stability and the first stable release
2
2
 
3
3
  Rehearsal leaves beta when the workflow it already promises is dependable. Stable
4
- `0.1.0` does not require every planned database, desktop interface, or paid feature.
4
+ `1.0.0` does not require every planned database, desktop interface, or paid feature.
5
5
 
6
6
  ## What becomes a compatibility promise
7
7
 
8
- Starting with `0.1.0`, Rehearsal treats these documented surfaces as public:
8
+ Starting with `1.0.0`, Rehearsal treats these documented surfaces as public:
9
9
 
10
10
  - CLI command names, flags, JSON result fields, and exit categories in
11
11
  [CLI commands](commands.md);
@@ -45,21 +45,29 @@ release.
45
45
 
46
46
  ## Promotion sequence
47
47
 
48
- The sequence below records the initial beta-to-stable policy, not current release
49
- progress. Historical version names explain that policy; they do not claim a candidate
50
- is ready or restart an observation window by themselves.
51
-
52
- 1. Ship beta.18 with only stability, release-safety, upgrade, and documentation work.
53
- 2. Freeze broad feature development on the release branch.
54
- 3. Publish a release candidate from protected `main` through the reviewed npm environment.
55
- 4. Run the complete Rehearsal Test Lab gate twice from clean isolated workspaces.
56
- 5. Perform one final package-only upgrade in a real consumer. The consumer must use the
57
- published package rather than duplicate Rehearsal machinery.
58
- 6. Keep the current candidate available for at least seven calendar days. Restart the
59
- window if a blocking package change is required. The container-lifecycle correction
60
- in `0.1.0-rc.2` restarts this observation window.
61
- 7. Publish `0.1.0` only when no blocking issue remains and the accepted registry artifact
62
- matches the tested artifact exactly.
48
+ Promotion is evidence-based, not a calendar countdown. The initial seven-day RC
49
+ wait was replaced after owner review; passing time alone cannot make a candidate
50
+ ready. The sequence below is policy, not a claim that its checks have passed.
51
+
52
+ 1. Freeze broad feature development on the release branch.
53
+ 2. Select one immutable candidate from the reviewed release branch. A release candidate
54
+ may be published from protected `main` through the reviewed npm environment; another
55
+ prerelease is not required solely to verify the final stable tarball.
56
+ 3. Run the complete Rehearsal Test Lab gate twice from clean isolated workspaces.
57
+ 4. Perform one final package-only check in a real consumer using the exact frozen
58
+ stable tarball, not duplicate Rehearsal machinery. Preserve its reviewed settings
59
+ and persistent data; a package check does not require resetting its databases.
60
+ 5. Review reports and existing scheduled Test Lab results for unresolved blockers.
61
+ A blocking correction needs its affected acceptance repeated against the exact
62
+ corrected candidate. Reuse sufficient unchanged evidence with its original scope;
63
+ do not relabel old or unavailable manual checks as new results. No minimum number
64
+ of calendar days replaces these checks.
65
+ 6. Publish `1.0.0` only when no blocking issue remains and the exact tarball has passed
66
+ the required release checks, consumer acceptance, exact-artifact authorization and
67
+ protected human publication approval.
68
+ 7. Independently verify registry bytes, provenance, signatures and channels, then
69
+ confirm the published package in the real consumer. Promotion is complete only
70
+ when the published artifact matches the tested tarball and these checks pass.
63
71
 
64
72
  Test Lab is the primary verification system. A real consumer is the final compatibility
65
73
  check, not a replacement for package-owned coverage. Real Google or GitHub consent does
@@ -69,6 +77,23 @@ not need to run on every build unless authentication behavior changes.
69
77
 
70
78
  Beta and release-candidate versions publish under the `beta` tag. Before a stable
71
79
  release exists, `latest` follows that reviewed prerelease so the ordinary install command
72
- remains useful. Once stable `0.1.0` exists, `latest` stays on the stable line and `beta`
80
+ remains useful. Once stable `1.0.0` exists, `latest` stays on the stable line and `beta`
73
81
  continues to identify the newest prerelease. Stable publication never rewrites the
74
82
  `beta` tag.
83
+
84
+ ## Upgrading from a 0.1 release candidate
85
+
86
+ The first stable release preserves rc.10's database runtime and schema formats and
87
+ corrects invalid-command diagnostics. A package upgrade
88
+ does not require resetting a sandbox or replacing its reviewed configuration, key or
89
+ baseline. Automatic compatible updates do not cross the 0.x-to-1.x version boundary;
90
+ review the changelog and deliberately select the stable version:
91
+
92
+ ```bash
93
+ npm install --save-dev --save-exact @rehearsal-db/core@1.0.0
94
+ npx --no-install rehearsal --version
95
+ npx --no-install rehearsal doctor
96
+ ```
97
+
98
+ Use the project's existing package manager and dependency location. Review its lockfile
99
+ and readiness output before the next sandbox operation; do not reset data just to update.
package/docs/tutorial.md CHANGED
@@ -117,6 +117,6 @@ npm run test:fixture
117
117
  The PostgreSQL equivalent is `npm run test:fixture:postgresql`.
118
118
 
119
119
  The release workflow also runs `npm run test:fixture:onboarding` from the packed package.
120
- That check creates fresh Supabase and PostgreSQL consumers, verifies beta configuration
120
+ That check creates fresh Supabase and PostgreSQL consumers, verifies reviewed configuration
121
121
  preservation, and exercises safe refusal and recovery for partial setup, occupied ports,
122
122
  malformed configuration, and missing credentials.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehearsal-db/core",
3
- "version": "0.1.0-rc.9",
3
+ "version": "1.0.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": {
@@ -76,7 +76,7 @@
76
76
  "test:fixture:standalone": "npm run build && node scripts/verification/run_scenario.mjs standalone",
77
77
  "test:fixture:privacy": "npm run build && node scripts/verification/run_scenario.mjs privacy",
78
78
  "test:fixture:resources": "npm run build && node scripts/verification/run_scenario.mjs resources",
79
- "release:verify": "node scripts/verification/release_gate.mjs",
79
+ "release:verify": "npm run build && node scripts/verification/release_gate.mjs",
80
80
  "check": "npm run check:syntax && npm run typecheck && npm run test && npm run docs:build && npm run format:check && npm run package:audit",
81
81
  "check:syntax": "find scripts tests -type f -name '*.mjs' -exec node --check {} +",
82
82
  "typecheck": "npm run build && tsc --project tsconfig.json",