@rehearsal-db/core 0.1.0-beta.14 → 0.1.0-beta.16

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 (236) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.md +1 -0
  3. package/dist/package.json +101 -0
  4. package/dist/scripts/install/create_root_config.d.mts +3 -0
  5. package/dist/scripts/install/create_root_config.mjs +13 -0
  6. package/dist/scripts/runtime/manage_database.d.mts +6 -0
  7. package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
  8. package/dist/src/application/session.d.mts +96 -0
  9. package/dist/src/application/session.mjs +380 -0
  10. package/dist/src/baseline/artifact.d.mts +119 -0
  11. package/dist/src/baseline/artifact.mjs +522 -0
  12. package/dist/src/baseline/builder.d.mts +19 -0
  13. package/dist/src/baseline/builder.mjs +142 -0
  14. package/dist/src/baseline/input_discovery.d.mts +13 -0
  15. package/dist/src/baseline/input_discovery.mjs +149 -0
  16. package/dist/src/baseline/policy_review.d.mts +56 -0
  17. package/dist/src/baseline/policy_review.mjs +73 -0
  18. package/dist/src/baseline/preparation.d.mts +63 -0
  19. package/dist/src/baseline/preparation.mjs +295 -0
  20. package/dist/src/baseline/privacy_contract.d.mts +147 -0
  21. package/dist/src/baseline/privacy_contract.mjs +2 -0
  22. package/dist/src/baseline/privacy_dependency_graph.d.mts +7 -0
  23. package/dist/src/baseline/privacy_dependency_graph.mjs +110 -0
  24. package/dist/src/baseline/privacy_engine.d.mts +5 -0
  25. package/dist/src/baseline/privacy_engine.mjs +4 -0
  26. package/dist/src/baseline/privacy_key.d.mts +7 -0
  27. package/dist/src/baseline/privacy_key.mjs +42 -0
  28. package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
  29. package/dist/src/baseline/privacy_pipeline.mjs +127 -0
  30. package/dist/src/baseline/privacy_policy.d.mts +6 -0
  31. package/dist/src/baseline/privacy_policy.mjs +237 -0
  32. package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
  33. package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
  34. package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
  35. package/dist/src/baseline/privacy_rule_registry.mjs +143 -0
  36. package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
  37. package/dist/src/baseline/privacy_structured_json.mjs +464 -0
  38. package/dist/src/baseline/privacy_transforms.d.mts +18 -0
  39. package/dist/src/baseline/privacy_transforms.mjs +142 -0
  40. package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
  41. package/dist/src/baseline/privacy_validation_shared.mjs +127 -0
  42. package/dist/src/baseline/sanitization_policy.d.mts +69 -0
  43. package/dist/src/baseline/sanitization_policy.mjs +227 -0
  44. package/dist/src/baseline/schema_snapshot.d.mts +19 -0
  45. package/dist/src/baseline/schema_snapshot.mjs +79 -0
  46. package/dist/src/cli/application_command_handlers.d.mts +8 -0
  47. package/dist/src/cli/application_command_handlers.mjs +52 -0
  48. package/dist/src/cli/arguments.d.mts +38 -0
  49. package/dist/src/cli/arguments.mjs +128 -0
  50. package/dist/src/cli/basic_command_handlers.d.mts +4 -0
  51. package/dist/src/cli/basic_command_handlers.mjs +56 -0
  52. package/dist/src/cli/cli_commands.d.mts +14 -0
  53. package/dist/src/cli/cli_commands.mjs +32 -0
  54. package/dist/src/cli/command_contract.d.mts +57 -0
  55. package/dist/src/cli/command_contract.mjs +39 -0
  56. package/dist/src/cli/command_output.d.mts +12 -0
  57. package/dist/src/cli/command_output.mjs +20 -0
  58. package/dist/src/cli/command_registry.d.mts +7 -0
  59. package/dist/src/cli/command_registry.mjs +14 -0
  60. package/dist/src/cli/guided.d.mts +53 -0
  61. package/dist/src/cli/guided.mjs +719 -0
  62. package/dist/src/cli/identity_command_handlers.d.mts +8 -0
  63. package/dist/src/cli/identity_command_handlers.mjs +43 -0
  64. package/dist/src/cli/init_command.d.mts +21 -0
  65. package/dist/src/cli/init_command.mjs +90 -0
  66. package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
  67. package/dist/src/cli/inspection_command_handlers.mjs +79 -0
  68. package/dist/src/cli/project_command_handlers.d.mts +11 -0
  69. package/dist/src/cli/project_command_handlers.mjs +92 -0
  70. package/dist/src/cli/project_commands.d.mts +159 -0
  71. package/dist/src/cli/project_commands.mjs +183 -0
  72. package/dist/src/cli/rehearsal.d.mts +7 -0
  73. package/dist/src/cli/rehearsal.mjs +190 -0
  74. package/dist/src/cli/renderers.d.mts +299 -0
  75. package/dist/src/cli/renderers.mjs +554 -0
  76. package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
  77. package/dist/src/cli/runtime_action_handlers.mjs +201 -0
  78. package/dist/src/cli/runtime_commands.d.mts +269 -0
  79. package/dist/src/cli/runtime_commands.mjs +601 -0
  80. package/dist/src/cli/source_command_handlers.d.mts +8 -0
  81. package/dist/src/cli/source_command_handlers.mjs +62 -0
  82. package/dist/src/cli/source_commands.d.mts +436 -0
  83. package/dist/src/cli/source_commands.mjs +301 -0
  84. package/dist/src/cli/terminal.d.mts +59 -0
  85. package/dist/src/cli/terminal.mjs +230 -0
  86. package/dist/src/identity/claim.d.mts +5 -0
  87. package/dist/src/identity/claim.mjs +4 -0
  88. package/dist/src/identity/claim_contract.d.mts +135 -0
  89. package/dist/src/identity/claim_contract.mjs +6 -0
  90. package/dist/src/identity/claim_executor.d.mts +10 -0
  91. package/dist/src/identity/claim_executor.mjs +358 -0
  92. package/dist/src/identity/claim_identity_lookup.d.mts +17 -0
  93. package/dist/src/identity/claim_identity_lookup.mjs +125 -0
  94. package/dist/src/identity/claim_plan.d.mts +8 -0
  95. package/dist/src/identity/claim_plan.mjs +61 -0
  96. package/dist/src/identity/claim_policy.d.mts +3 -0
  97. package/dist/src/identity/claim_policy.mjs +377 -0
  98. package/dist/src/identity/claim_sql.d.mts +10 -0
  99. package/dist/src/identity/claim_sql.mjs +46 -0
  100. package/dist/src/identity/claim_verification.d.mts +13 -0
  101. package/dist/src/identity/claim_verification.mjs +35 -0
  102. package/dist/src/identity/provider_configuration.d.mts +37 -0
  103. package/dist/src/identity/provider_configuration.mjs +132 -0
  104. package/dist/src/identity/storage.d.mts +21 -0
  105. package/dist/src/identity/storage.mjs +122 -0
  106. package/dist/src/index.d.mts +4 -0
  107. package/dist/src/index.mjs +3 -0
  108. package/dist/src/project/configuration.d.mts +13 -0
  109. package/dist/src/project/configuration.mjs +9 -0
  110. package/dist/src/project/configuration_contract.d.mts +125 -0
  111. package/dist/src/project/configuration_contract.mjs +27 -0
  112. package/dist/src/project/configuration_discovery.d.mts +7 -0
  113. package/dist/src/project/configuration_discovery.mjs +37 -0
  114. package/dist/src/project/configuration_normalization.d.mts +107 -0
  115. package/dist/src/project/configuration_normalization.mjs +302 -0
  116. package/dist/src/project/configuration_rendering.d.mts +14 -0
  117. package/dist/src/project/configuration_rendering.mjs +224 -0
  118. package/dist/src/project/configuration_resolution.d.mts +125 -0
  119. package/dist/src/project/configuration_resolution.mjs +81 -0
  120. package/dist/src/project/configuration_schema.d.mts +22 -0
  121. package/dist/src/project/configuration_schema.mjs +160 -0
  122. package/dist/src/project/configuration_validation.d.mts +22 -0
  123. package/dist/src/project/configuration_validation.mjs +136 -0
  124. package/dist/src/project/install_scaffold.d.mts +42 -0
  125. package/dist/src/project/install_scaffold.mjs +263 -0
  126. package/dist/src/project/project_detection.d.mts +14 -0
  127. package/dist/src/project/project_detection.mjs +47 -0
  128. package/dist/src/project/setup.d.mts +87 -0
  129. package/dist/src/project/setup.mjs +414 -0
  130. package/dist/src/project/support_report.d.mts +88 -0
  131. package/dist/src/project/support_report.mjs +83 -0
  132. package/dist/src/runtime/cleanup.d.mts +85 -0
  133. package/dist/src/runtime/cleanup.mjs +311 -0
  134. package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
  135. package/dist/src/runtime/lifecycle_engine.mjs +114 -0
  136. package/dist/src/runtime/migration_history.d.mts +51 -0
  137. package/dist/src/runtime/migration_history.mjs +170 -0
  138. package/dist/src/runtime/plan.d.mts +134 -0
  139. package/dist/src/runtime/plan.mjs +637 -0
  140. package/dist/src/runtime/policy.d.mts +66 -0
  141. package/dist/src/runtime/policy.mjs +369 -0
  142. package/dist/src/runtime/restore.d.mts +22 -0
  143. package/dist/src/runtime/restore.mjs +276 -0
  144. package/dist/src/runtime/service_environment.d.mts +11 -0
  145. package/dist/src/runtime/service_environment.mjs +58 -0
  146. package/dist/src/runtime/topology.d.mts +51 -0
  147. package/dist/src/runtime/topology.mjs +128 -0
  148. package/dist/src/shared/diagnostics.d.mts +97 -0
  149. package/dist/src/shared/diagnostics.mjs +170 -0
  150. package/dist/src/shared/human_output.d.mts +2 -0
  151. package/{src → dist/src}/shared/human_output.mjs +1 -3
  152. package/dist/src/shared/node_runtime.d.mts +14 -0
  153. package/dist/src/shared/node_runtime.mjs +25 -0
  154. package/dist/src/shared/operation_guard.d.mts +22 -0
  155. package/dist/src/shared/operation_guard.mjs +150 -0
  156. package/dist/src/shared/process_environment.d.mts +15 -0
  157. package/dist/src/shared/process_environment.mjs +50 -0
  158. package/dist/src/source/access.d.mts +165 -0
  159. package/dist/src/source/access.mjs +602 -0
  160. package/dist/src/source/asset_transfer.d.mts +27 -0
  161. package/dist/src/source/asset_transfer.mjs +202 -0
  162. package/dist/src/source/baseline.d.mts +66 -0
  163. package/dist/src/source/baseline.mjs +372 -0
  164. package/dist/src/source/external_reader.d.mts +42 -0
  165. package/dist/src/source/external_reader.mjs +278 -0
  166. package/dist/src/source/postgresql_access.d.mts +86 -0
  167. package/dist/src/source/postgresql_access.mjs +454 -0
  168. package/dist/src/source/privacy_paths.d.mts +10 -0
  169. package/dist/src/source/privacy_paths.mjs +27 -0
  170. package/dist/src/targets/driver.d.mts +2 -0
  171. package/dist/src/targets/driver.mjs +13 -0
  172. package/dist/src/targets/postgresql.d.mts +5 -0
  173. package/dist/src/targets/postgresql.mjs +620 -0
  174. package/dist/src/targets/supabase.d.mts +8 -0
  175. package/dist/src/targets/supabase.mjs +627 -0
  176. package/dist/src/targets/supabase_environment.d.mts +49 -0
  177. package/dist/src/targets/supabase_environment.mjs +162 -0
  178. package/dist/src/targets/target.d.mts +33 -0
  179. package/dist/src/targets/target.mjs +39 -0
  180. package/docs/README.md +1 -0
  181. package/docs/architecture.md +133 -10
  182. package/docs/configuration.md +9 -3
  183. package/docs/index.md +1 -0
  184. package/docs/releasing.md +8 -3
  185. package/docs/typescript.md +134 -0
  186. package/package.json +36 -29
  187. package/types/configuration.d.mts +5 -0
  188. package/scripts/install/create_root_config.mjs +0 -19
  189. package/src/README.md +0 -17
  190. package/src/application/session.mjs +0 -435
  191. package/src/baseline/artifact.mjs +0 -684
  192. package/src/baseline/builder.mjs +0 -164
  193. package/src/baseline/input_discovery.mjs +0 -155
  194. package/src/baseline/policy_review.mjs +0 -92
  195. package/src/baseline/preparation.mjs +0 -323
  196. package/src/baseline/privacy_engine.mjs +0 -1399
  197. package/src/baseline/privacy_structured_json.mjs +0 -554
  198. package/src/baseline/sanitization_policy.mjs +0 -299
  199. package/src/baseline/schema_snapshot.mjs +0 -113
  200. package/src/cli/arguments.mjs +0 -126
  201. package/src/cli/guided.mjs +0 -814
  202. package/src/cli/init_command.mjs +0 -94
  203. package/src/cli/rehearsal.mjs +0 -972
  204. package/src/cli/renderers.mjs +0 -638
  205. package/src/cli/runtime_commands.mjs +0 -707
  206. package/src/cli/source_commands.mjs +0 -355
  207. package/src/cli/terminal.mjs +0 -275
  208. package/src/identity/claim.mjs +0 -1165
  209. package/src/identity/provider_configuration.mjs +0 -177
  210. package/src/identity/storage.mjs +0 -165
  211. package/src/project/configuration.d.mts +0 -164
  212. package/src/project/configuration.mjs +0 -1337
  213. package/src/project/install_scaffold.mjs +0 -325
  214. package/src/project/setup.mjs +0 -474
  215. package/src/project/support_report.mjs +0 -105
  216. package/src/runtime/cleanup.mjs +0 -381
  217. package/src/runtime/migration_history.mjs +0 -220
  218. package/src/runtime/plan.mjs +0 -722
  219. package/src/runtime/policy.mjs +0 -438
  220. package/src/runtime/restore.mjs +0 -337
  221. package/src/runtime/service_environment.mjs +0 -82
  222. package/src/runtime/topology.mjs +0 -177
  223. package/src/shared/diagnostics.mjs +0 -207
  224. package/src/shared/node_runtime.mjs +0 -31
  225. package/src/shared/operation_guard.mjs +0 -162
  226. package/src/shared/process_environment.mjs +0 -68
  227. package/src/source/access.mjs +0 -798
  228. package/src/source/asset_transfer.mjs +0 -259
  229. package/src/source/baseline.mjs +0 -482
  230. package/src/source/external_reader.mjs +0 -368
  231. package/src/source/postgresql_access.mjs +0 -638
  232. package/src/source/privacy_paths.mjs +0 -38
  233. package/src/targets/postgresql.mjs +0 -808
  234. package/src/targets/supabase.mjs +0 -840
  235. package/src/targets/supabase_environment.mjs +0 -229
  236. package/src/targets/target.mjs +0 -66
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Purpose: Preview and apply narrowly scoped Rehearsal cleanup without global
3
+ * Docker pruning or deletion of unowned database resources.
4
+ */
5
+ import { spawnSync } from "node:child_process";
6
+ import { createHash } from "node:crypto";
7
+ import { stat } from "node:fs/promises";
8
+ import { planBaselineGenerationPrune, pruneBaselineGenerations, } from "../baseline/artifact.mjs";
9
+ import { loadRehearsalConfig } from "../project/configuration.mjs";
10
+ import { createCleanProcessEnvironment } from "../shared/process_environment.mjs";
11
+ const hasErrorCode = (error, code) => typeof error === "object" &&
12
+ error !== null &&
13
+ "code" in error &&
14
+ error.code === code;
15
+ const SUPABASE_IMAGE_PREFIX = "public.ecr.aws/supabase/";
16
+ const commandResult = (command, args, { projectRoot }) => spawnSync(command, args, {
17
+ cwd: projectRoot,
18
+ encoding: "utf8",
19
+ env: createCleanProcessEnvironment(),
20
+ maxBuffer: 32 * 1024 * 1024,
21
+ stdio: ["ignore", "pipe", "pipe"],
22
+ });
23
+ const runDocker = (args, { projectRoot, allowFailure = false, }) => {
24
+ const result = commandResult("docker", args, { projectRoot });
25
+ if (!allowFailure && (result.error || result.status !== 0)) {
26
+ const detail = [result.stdout, result.stderr]
27
+ .filter(Boolean)
28
+ .join("\n")
29
+ .trim();
30
+ throw new Error(`docker ${args.join(" ")} failed${detail ? `:\n${detail}` : "."}`);
31
+ }
32
+ return result;
33
+ };
34
+ const nonEmptyLines = (value) => String(value ?? "")
35
+ .split("\n")
36
+ .map((line) => line.trim())
37
+ .filter(Boolean);
38
+ const dockerAvailable = (projectRoot) => runDocker(["info"], { projectRoot, allowFailure: true }).status === 0;
39
+ export const parsePosixDiskUsage = (output) => {
40
+ const columns = nonEmptyLines(output).at(-1)?.split(/\s+/u) ?? [];
41
+ if (columns.length < 6)
42
+ return null;
43
+ const totalKiB = Number(columns[1]);
44
+ const usedKiB = Number(columns[2]);
45
+ const availableKiB = Number(columns[3]);
46
+ const usedPercent = Number.parseInt(columns[4], 10);
47
+ if (![totalKiB, usedKiB, availableKiB, usedPercent].every(Number.isFinite)) {
48
+ return null;
49
+ }
50
+ return {
51
+ totalBytes: totalKiB * 1024,
52
+ usedBytes: usedKiB * 1024,
53
+ availableBytes: availableKiB * 1024,
54
+ usedPercent,
55
+ };
56
+ };
57
+ const inspectContainerDisk = (projectRoot) => {
58
+ const context = commandResult("docker", ["context", "show"], {
59
+ projectRoot,
60
+ });
61
+ if (context.status !== 0 || context.stdout.trim() !== "colima")
62
+ return null;
63
+ const result = commandResult("colima", ["ssh", "--", "df", "-Pk", "/var/lib/docker"], { projectRoot });
64
+ const usage = result.status === 0 ? parsePosixDiskUsage(result.stdout) : null;
65
+ return usage ? { backend: "colima", ...usage } : null;
66
+ };
67
+ const inspectDockerUsage = (projectRoot) => {
68
+ if (!dockerAvailable(projectRoot)) {
69
+ return { available: false, resources: [], disk: null };
70
+ }
71
+ const result = runDocker(["system", "df", "--format", "{{json .}}"], {
72
+ projectRoot,
73
+ });
74
+ return {
75
+ available: true,
76
+ resources: nonEmptyLines(result.stdout).map((line) => JSON.parse(line)),
77
+ disk: inspectContainerDisk(projectRoot),
78
+ };
79
+ };
80
+ const inspectUsedImageIds = (projectRoot) => {
81
+ const containers = nonEmptyLines(runDocker(["container", "ls", "--all", "--quiet"], { projectRoot }).stdout);
82
+ if (containers.length === 0)
83
+ return new Set();
84
+ return new Set(nonEmptyLines(runDocker(["container", "inspect", "--format", "{{.Image}}", ...containers], { projectRoot }).stdout));
85
+ };
86
+ const splitImageTag = (value) => {
87
+ const separator = value.lastIndexOf(":");
88
+ if (separator <= value.lastIndexOf("/"))
89
+ return null;
90
+ return {
91
+ repository: value.slice(0, separator),
92
+ tag: value.slice(separator + 1),
93
+ };
94
+ };
95
+ export const selectOlderUnusedSupabaseImages = ({ images, usedImageIds, }) => {
96
+ const byRepository = new Map();
97
+ for (const image of images) {
98
+ for (const fullTag of image.RepoTags ?? []) {
99
+ const parsed = splitImageTag(fullTag);
100
+ if (!parsed?.repository.startsWith(SUPABASE_IMAGE_PREFIX))
101
+ continue;
102
+ const entries = byRepository.get(parsed.repository) ?? [];
103
+ entries.push({
104
+ id: image.Id,
105
+ createdAt: image.Created,
106
+ bytes: image.Size,
107
+ fullTag,
108
+ });
109
+ byRepository.set(parsed.repository, entries);
110
+ }
111
+ }
112
+ const candidatesById = new Map();
113
+ for (const entries of byRepository.values()) {
114
+ const newestId = [...entries].sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0]?.id;
115
+ for (const entry of entries) {
116
+ if (entry.id === newestId || usedImageIds.has(entry.id))
117
+ continue;
118
+ const existing = candidatesById.get(entry.id) ?? {
119
+ id: entry.id,
120
+ createdAt: entry.createdAt,
121
+ bytes: entry.bytes,
122
+ tags: [],
123
+ };
124
+ existing.tags.push(entry.fullTag);
125
+ candidatesById.set(entry.id, existing);
126
+ }
127
+ }
128
+ return [...candidatesById.values()]
129
+ .map((entry) => ({ ...entry, tags: [...new Set(entry.tags)].sort() }))
130
+ .sort((left, right) => left.createdAt.localeCompare(right.createdAt));
131
+ };
132
+ const inspectUnusedImages = (projectRoot) => {
133
+ const imageIds = [
134
+ ...new Set(nonEmptyLines(runDocker(["image", "ls", "--quiet", "--no-trunc"], {
135
+ projectRoot,
136
+ }).stdout)),
137
+ ];
138
+ if (imageIds.length === 0)
139
+ return [];
140
+ const images = nonEmptyLines(runDocker(["image", "inspect", "--format", "{{json .}}", ...imageIds], {
141
+ projectRoot,
142
+ }).stdout).map((line) => JSON.parse(line));
143
+ return selectOlderUnusedSupabaseImages({
144
+ images,
145
+ usedImageIds: inspectUsedImageIds(projectRoot),
146
+ });
147
+ };
148
+ const runtimeDetected = async ({ config, paths, projectRoot, }) => {
149
+ try {
150
+ await stat(paths.runtimeWorkdir);
151
+ return true;
152
+ }
153
+ catch (error) {
154
+ if (!hasErrorCode(error, "ENOENT"))
155
+ throw error;
156
+ }
157
+ if (!dockerAvailable(projectRoot))
158
+ return false;
159
+ if (config.runtime.target === "postgresql") {
160
+ const containers = runDocker([
161
+ "container",
162
+ "ls",
163
+ "--all",
164
+ "--filter",
165
+ `label=com.rehearsal-db.project=${config.runtime.projectId}`,
166
+ "--quiet",
167
+ ], { projectRoot });
168
+ const volumes = runDocker([
169
+ "volume",
170
+ "ls",
171
+ "--filter",
172
+ `label=com.rehearsal-db.project=${config.runtime.projectId}`,
173
+ "--quiet",
174
+ ], { projectRoot });
175
+ return (nonEmptyLines(containers.stdout).length > 0 ||
176
+ nonEmptyLines(volumes.stdout).length > 0);
177
+ }
178
+ const containers = runDocker([
179
+ "container",
180
+ "ls",
181
+ "--all",
182
+ "--filter",
183
+ `label=com.supabase.cli.project=${config.runtime.projectId}`,
184
+ "--quiet",
185
+ ], { projectRoot });
186
+ const volumes = nonEmptyLines(runDocker(["volume", "ls", "--format", "{{.Name}}"], { projectRoot })
187
+ .stdout);
188
+ return (nonEmptyLines(containers.stdout).length > 0 ||
189
+ volumes.some((name) => name.endsWith(`_${config.runtime.projectId}`)));
190
+ };
191
+ const inspectBaselineCleanup = async ({ artifactRoot, retain, }) => {
192
+ try {
193
+ return {
194
+ available: true,
195
+ ...(await planBaselineGenerationPrune({ artifactRoot, retain })),
196
+ };
197
+ }
198
+ catch (error) {
199
+ if (hasErrorCode(error, "ENOENT")) {
200
+ return { available: false, retained: [], removed: [] };
201
+ }
202
+ throw error;
203
+ }
204
+ };
205
+ const cleanupIdentity = (plan) => ({
206
+ projectId: plan.projectId,
207
+ retainBaselineGenerations: plan.retainBaselineGenerations,
208
+ baselineGenerations: plan.baselines.removed,
209
+ runtime: plan.runtime.included && plan.runtime.detected,
210
+ images: plan.images.candidates.map(({ id, tags }) => ({ id, tags })),
211
+ });
212
+ const cleanupDigest = (plan) => createHash("sha256")
213
+ .update(JSON.stringify(cleanupIdentity(plan)))
214
+ .digest("hex");
215
+ export const planRehearsalCleanup = async ({ projectRoot = process.cwd(), configPath, includeRuntime = false, includeImages = false, inspection = { inspectDockerUsage, runtimeDetected }, } = {}) => {
216
+ const loaded = await loadRehearsalConfig({
217
+ projectRoot,
218
+ ...(configPath === undefined ? {} : { configPath }),
219
+ });
220
+ const docker = inspection.inspectDockerUsage(loaded.projectRoot);
221
+ const baselines = await inspectBaselineCleanup({
222
+ artifactRoot: loaded.paths.artifactDirectory,
223
+ retain: loaded.config.cleanup.retainBaselineGenerations,
224
+ });
225
+ const images = {
226
+ requested: includeImages,
227
+ inspected: includeImages && docker.available,
228
+ candidates: includeImages && docker.available
229
+ ? inspectUnusedImages(loaded.projectRoot)
230
+ : [],
231
+ };
232
+ const plan = {
233
+ project: loaded.config.project.name,
234
+ projectId: loaded.config.runtime.projectId,
235
+ target: loaded.config.runtime.target,
236
+ retainBaselineGenerations: loaded.config.cleanup.retainBaselineGenerations,
237
+ baselines,
238
+ runtime: {
239
+ included: includeRuntime,
240
+ detected: await inspection.runtimeDetected({ ...loaded }),
241
+ },
242
+ images,
243
+ docker,
244
+ };
245
+ return Object.freeze({ ...plan, digest: cleanupDigest(plan) });
246
+ };
247
+ const removePlannedImages = ({ candidates, projectRoot, }) => {
248
+ if (candidates.length === 0)
249
+ return [];
250
+ const usedImageIds = inspectUsedImageIds(projectRoot);
251
+ const removed = [];
252
+ for (const candidate of candidates) {
253
+ if (usedImageIds.has(candidate.id)) {
254
+ throw new Error(`Cleanup stopped because a container began using ${candidate.id}. Preview the cleanup again.`);
255
+ }
256
+ for (const tag of candidate.tags) {
257
+ const inspection = runDocker(["image", "inspect", "--format", "{{.Id}}", tag], { projectRoot, allowFailure: true });
258
+ if (inspection.status !== 0)
259
+ continue;
260
+ if (inspection.stdout.trim() !== candidate.id) {
261
+ throw new Error(`Cleanup stopped because ${tag} changed after the preview. Preview the cleanup again.`);
262
+ }
263
+ runDocker(["image", "rm", tag], { projectRoot });
264
+ removed.push(tag);
265
+ }
266
+ }
267
+ return removed;
268
+ };
269
+ export const applyRehearsalCleanup = async ({ plan, confirmation, projectRoot = process.cwd(), configPath, removeRuntime = async () => undefined, inspection, } = {}) => {
270
+ if (!plan || confirmation !== plan.digest) {
271
+ throw new Error("Cleanup requires the exact --confirm-cleanup digest from a fresh preview. Nothing was removed.");
272
+ }
273
+ const refreshed = await planRehearsalCleanup({
274
+ projectRoot,
275
+ ...(configPath === undefined ? {} : { configPath }),
276
+ includeRuntime: plan.runtime.included,
277
+ includeImages: plan.images.inspected,
278
+ ...(inspection ? { inspection } : {}),
279
+ });
280
+ if (refreshed.digest !== plan.digest) {
281
+ throw new Error("Cleanup targets changed after the preview. Nothing was removed; preview the cleanup again.");
282
+ }
283
+ if (plan.images.requested && !refreshed.images.inspected) {
284
+ throw new Error("Docker is unavailable, so Rehearsal cannot verify or remove unused images.");
285
+ }
286
+ if (refreshed.runtime.included && refreshed.runtime.detected) {
287
+ await removeRuntime();
288
+ }
289
+ const loaded = await loadRehearsalConfig({
290
+ projectRoot,
291
+ ...(configPath === undefined ? {} : { configPath }),
292
+ });
293
+ const baselines = refreshed.baselines.available
294
+ ? await pruneBaselineGenerations({
295
+ artifactRoot: loaded.paths.artifactDirectory,
296
+ retain: refreshed.retainBaselineGenerations,
297
+ expectedRemoved: refreshed.baselines.removed,
298
+ })
299
+ : { retained: [], removed: [] };
300
+ const images = removePlannedImages({
301
+ candidates: refreshed.images.candidates,
302
+ projectRoot: loaded.projectRoot,
303
+ });
304
+ return {
305
+ project: refreshed.project,
306
+ projectId: refreshed.projectId,
307
+ baselines,
308
+ runtimeRemoved: refreshed.runtime.included && refreshed.runtime.detected,
309
+ imagesRemoved: images,
310
+ };
311
+ };
@@ -0,0 +1,63 @@
1
+ /** Shared, target-neutral orchestration for disposable runtime lifecycles. */
2
+ import type { PathLike } from "node:fs";
3
+ import type { ActiveBaselinePaths, BaselineManifest } from "../baseline/artifact.mjs";
4
+ import type { RuntimeSanitizationPolicy } from "../baseline/sanitization_policy.mjs";
5
+ import type { MigrationFileEntry } from "./migration_history.mjs";
6
+ export type RuntimeLifecycleAction = "reset" | "start" | "status" | "candidates" | "migrate" | "verify" | "run" | "stop" | "discard";
7
+ export interface ActiveRuntimeInput {
8
+ readonly baseline: BaselineManifest;
9
+ readonly paths: ActiveBaselinePaths;
10
+ readonly manifest: RuntimeSanitizationPolicy;
11
+ }
12
+ export interface CandidateMigrationPlan {
13
+ readonly currentFiles: readonly MigrationFileEntry[];
14
+ readonly candidates: readonly MigrationFileEntry[];
15
+ readonly candidateSha256: string;
16
+ }
17
+ export interface RuntimeLifecycleOperations {
18
+ readonly reset: () => Promise<unknown>;
19
+ readonly start: () => Promise<unknown>;
20
+ readonly status: () => Promise<unknown>;
21
+ readonly candidates: () => Promise<unknown>;
22
+ readonly migrate: () => Promise<unknown>;
23
+ readonly verify: () => Promise<unknown>;
24
+ readonly stop: () => Promise<unknown>;
25
+ readonly discard: () => Promise<unknown>;
26
+ }
27
+ export declare const readActiveRuntimeInput: ({ artifactRoot, manifestPath, validate, }: {
28
+ artifactRoot: string;
29
+ manifestPath: PathLike;
30
+ validate?: (input: ActiveRuntimeInput) => void | Promise<void>;
31
+ }) => Promise<ActiveRuntimeInput>;
32
+ export declare const readCandidateMigrationPlan: ({ baseline, migrationDirectory, }: {
33
+ baseline: BaselineManifest;
34
+ migrationDirectory: URL;
35
+ }) => Promise<CandidateMigrationPlan>;
36
+ export declare const assertCandidateConfirmation: ({ plan, confirmation, allowUnconfirmedEmpty, }: {
37
+ plan: CandidateMigrationPlan;
38
+ confirmation: string | undefined;
39
+ allowUnconfirmedEmpty?: boolean;
40
+ }) => void;
41
+ export declare const writeCandidateMigrationReceipt: ({ path, target, baseline, plan, }: {
42
+ path: PathLike;
43
+ target: string;
44
+ baseline: BaselineManifest;
45
+ plan: CandidateMigrationPlan;
46
+ }) => Promise<void>;
47
+ export declare const writeRuntimeMarker: ({ path, marker, }: {
48
+ path: PathLike;
49
+ marker: Readonly<Record<string, unknown>>;
50
+ }) => Promise<void>;
51
+ export declare const readMatchingRuntimeMarker: ({ path, expected, mismatchMessage, }: {
52
+ path: PathLike;
53
+ expected: Readonly<Record<string, unknown>>;
54
+ mismatchMessage: string;
55
+ }) => Promise<Readonly<Record<string, unknown>>>;
56
+ export declare const withOwnedRuntimeRollback: <Result>({ operation, rollback, }: {
57
+ operation: () => Promise<Result>;
58
+ rollback: () => Promise<unknown>;
59
+ }) => Promise<Result>;
60
+ export declare const runRuntimeLifecycle: ({ action, operations, }: {
61
+ action: string;
62
+ operations: RuntimeLifecycleOperations;
63
+ }) => Promise<void>;
@@ -0,0 +1,114 @@
1
+ /** Shared, target-neutral orchestration for disposable runtime lifecycles. */
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { resolveActiveBaselinePaths, verifyActiveBaseline, } from "../baseline/artifact.mjs";
4
+ import { readBoundRuntimeSanitizationPolicy } from "../baseline/sanitization_policy.mjs";
5
+ import { readMigrationFileInventory } from "./migration_history.mjs";
6
+ import { createCandidateMigrationReceipt } from "./restore.mjs";
7
+ export const readActiveRuntimeInput = async ({ artifactRoot, manifestPath, validate, }) => {
8
+ const [baseline, paths, policyBytes] = await Promise.all([
9
+ verifyActiveBaseline({ artifactRoot }),
10
+ resolveActiveBaselinePaths({ artifactRoot }),
11
+ readFile(manifestPath),
12
+ ]);
13
+ const manifest = readBoundRuntimeSanitizationPolicy({
14
+ bytes: policyBytes,
15
+ expectedSha256: baseline.sanitizationPolicySha256,
16
+ });
17
+ const input = { baseline, paths, manifest };
18
+ await validate?.(input);
19
+ return input;
20
+ };
21
+ export const readCandidateMigrationPlan = async ({ baseline, migrationDirectory, }) => {
22
+ const currentFiles = await readMigrationFileInventory(migrationDirectory);
23
+ return {
24
+ currentFiles,
25
+ ...createCandidateMigrationReceipt({
26
+ baselineManifest: baseline,
27
+ currentFiles,
28
+ }),
29
+ };
30
+ };
31
+ export const assertCandidateConfirmation = ({ plan, confirmation, allowUnconfirmedEmpty = false, }) => {
32
+ if (allowUnconfirmedEmpty && plan.candidates.length === 0)
33
+ return;
34
+ if (confirmation !== plan.candidateSha256) {
35
+ throw new Error("Candidate migration confirmation is missing or does not match the exact SHA-256.");
36
+ }
37
+ };
38
+ export const writeCandidateMigrationReceipt = async ({ path, target, baseline, plan, }) => {
39
+ await writeFile(path, `${JSON.stringify({
40
+ formatVersion: 1,
41
+ target,
42
+ baselineGenerationId: baseline.generationId,
43
+ candidateSha256: plan.candidateSha256,
44
+ candidates: plan.candidates.map((candidate) => ({
45
+ filename: candidate.filename,
46
+ sha256: candidate.fileSha256,
47
+ })),
48
+ }, null, "\t")}\n`, { mode: 0o600 });
49
+ };
50
+ export const writeRuntimeMarker = async ({ path, marker, }) => {
51
+ await writeFile(path, `${JSON.stringify(marker, null, "\t")}\n`, {
52
+ mode: 0o600,
53
+ });
54
+ };
55
+ export const readMatchingRuntimeMarker = async ({ path, expected, mismatchMessage, }) => {
56
+ let marker;
57
+ try {
58
+ marker = JSON.parse(await readFile(path, "utf8"));
59
+ }
60
+ catch (error) {
61
+ throw new Error(mismatchMessage, { cause: error });
62
+ }
63
+ if (marker === null ||
64
+ typeof marker !== "object" ||
65
+ Array.isArray(marker) ||
66
+ Object.entries(expected).some(([key, value]) => !Object.is(marker[key], value))) {
67
+ throw new Error(mismatchMessage);
68
+ }
69
+ return marker;
70
+ };
71
+ export const withOwnedRuntimeRollback = async ({ operation, rollback, }) => {
72
+ try {
73
+ return await operation();
74
+ }
75
+ catch (error) {
76
+ await rollback().catch(() => undefined);
77
+ throw error;
78
+ }
79
+ };
80
+ export const runRuntimeLifecycle = async ({ action, operations, }) => {
81
+ switch (action) {
82
+ case "reset":
83
+ await operations.reset();
84
+ return;
85
+ case "start":
86
+ await operations.start();
87
+ return;
88
+ case "status":
89
+ await operations.status();
90
+ return;
91
+ case "candidates":
92
+ await operations.candidates();
93
+ return;
94
+ case "migrate":
95
+ await operations.migrate();
96
+ return;
97
+ case "verify":
98
+ await operations.verify();
99
+ return;
100
+ case "run":
101
+ await operations.reset();
102
+ await operations.migrate();
103
+ await operations.verify();
104
+ return;
105
+ case "stop":
106
+ await operations.stop();
107
+ return;
108
+ case "discard":
109
+ await operations.discard();
110
+ return;
111
+ default:
112
+ throw new Error(`Unknown Rehearsal database action: ${action}`);
113
+ }
114
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Purpose: Build and compare content-addressed application migration histories for
3
+ * Rehearsal without trusting timestamp-only migration alignment. Do not run directly;
4
+ * this module is reusable script infrastructure.
5
+ */
6
+ export interface MigrationHistoryEntry {
7
+ version: string;
8
+ name: string;
9
+ }
10
+ export interface MigrationFileEntry extends MigrationHistoryEntry {
11
+ filename: string;
12
+ fileSha256: string;
13
+ }
14
+ export interface MigrationSourceEntry extends MigrationFileEntry {
15
+ content: string;
16
+ }
17
+ export interface MigrationLedgerEntry extends MigrationHistoryEntry {
18
+ statementCount: number;
19
+ statementSha256: string;
20
+ }
21
+ export interface MigrationReplayEntry extends MigrationFileEntry, MigrationLedgerEntry {
22
+ }
23
+ export interface MigrationReplayReceipt {
24
+ entries: MigrationReplayEntry[];
25
+ historySha256: string;
26
+ }
27
+ export declare const readMigrationFileInventory: (directory: URL) => Promise<MigrationFileEntry[]>;
28
+ export declare const readMigrationSourceBundle: ({ directory, sourceLedger, }: {
29
+ directory: URL;
30
+ sourceLedger: readonly MigrationLedgerEntry[];
31
+ }) => Promise<MigrationSourceEntry[]>;
32
+ export declare const buildMigrationLedgerInventory: (rows: unknown, label?: string) => MigrationLedgerEntry[];
33
+ export declare const createMigrationReplayReceipt: ({ files, ledger, }: {
34
+ files: readonly MigrationFileEntry[];
35
+ ledger: readonly MigrationLedgerEntry[];
36
+ }) => MigrationReplayReceipt;
37
+ export declare const compareSourceToReplay: ({ sourceLedger, replayReceipt, snapshotSchemaSha256, }: {
38
+ sourceLedger: readonly MigrationLedgerEntry[];
39
+ replayReceipt: MigrationReplayReceipt;
40
+ snapshotSchemaSha256?: string;
41
+ }) => {
42
+ cutoff: string;
43
+ candidates: MigrationReplayEntry[];
44
+ candidateSha256: string;
45
+ historicalStatementDifferences: number;
46
+ snapshotSchemaSha256: string | null;
47
+ };
48
+ export declare const assertNoCrossDatabaseMigrationVersions: ({ applicationFiles, apiFiles, }: {
49
+ applicationFiles: readonly MigrationHistoryEntry[];
50
+ apiFiles: readonly MigrationHistoryEntry[];
51
+ }) => void;