@rehearsal-db/core 0.1.0-beta.9 → 0.1.0-rc.2

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 (247) hide show
  1. package/CHANGELOG.md +319 -1
  2. package/COMPATIBILITY.md +15 -0
  3. package/README.md +73 -16
  4. package/dist/package.json +100 -0
  5. package/dist/scripts/install/create_root_config.d.mts +3 -0
  6. package/dist/scripts/install/create_root_config.mjs +13 -0
  7. package/dist/scripts/runtime/manage_database.d.mts +6 -0
  8. package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
  9. package/dist/src/application/interactive_task.d.mts +30 -0
  10. package/dist/src/application/interactive_task.mjs +83 -0
  11. package/dist/src/application/session.d.mts +97 -0
  12. package/dist/src/application/session.mjs +380 -0
  13. package/dist/src/baseline/artifact.d.mts +119 -0
  14. package/dist/src/baseline/artifact.mjs +522 -0
  15. package/dist/src/baseline/builder.d.mts +19 -0
  16. package/dist/src/baseline/builder.mjs +142 -0
  17. package/dist/src/baseline/input_discovery.d.mts +13 -0
  18. package/dist/src/baseline/input_discovery.mjs +149 -0
  19. package/dist/src/baseline/policy_review.d.mts +56 -0
  20. package/dist/src/baseline/policy_review.mjs +73 -0
  21. package/dist/src/baseline/preparation.d.mts +63 -0
  22. package/dist/src/baseline/preparation.mjs +295 -0
  23. package/dist/src/baseline/privacy_contract.d.mts +147 -0
  24. package/dist/src/baseline/privacy_contract.mjs +2 -0
  25. package/dist/src/baseline/privacy_dependency_graph.d.mts +7 -0
  26. package/dist/src/baseline/privacy_dependency_graph.mjs +110 -0
  27. package/dist/src/baseline/privacy_engine.d.mts +5 -0
  28. package/dist/src/baseline/privacy_engine.mjs +4 -0
  29. package/dist/src/baseline/privacy_key.d.mts +7 -0
  30. package/dist/src/baseline/privacy_key.mjs +42 -0
  31. package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
  32. package/dist/src/baseline/privacy_pipeline.mjs +127 -0
  33. package/dist/src/baseline/privacy_policy.d.mts +6 -0
  34. package/dist/src/baseline/privacy_policy.mjs +237 -0
  35. package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
  36. package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
  37. package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
  38. package/dist/src/baseline/privacy_rule_registry.mjs +143 -0
  39. package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
  40. package/dist/src/baseline/privacy_structured_json.mjs +464 -0
  41. package/dist/src/baseline/privacy_transforms.d.mts +18 -0
  42. package/dist/src/baseline/privacy_transforms.mjs +142 -0
  43. package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
  44. package/dist/src/baseline/privacy_validation_shared.mjs +127 -0
  45. package/dist/src/baseline/sanitization_policy.d.mts +69 -0
  46. package/dist/src/baseline/sanitization_policy.mjs +227 -0
  47. package/dist/src/baseline/schema_snapshot.d.mts +19 -0
  48. package/dist/src/baseline/schema_snapshot.mjs +79 -0
  49. package/dist/src/cli/application_command_handlers.d.mts +8 -0
  50. package/dist/src/cli/application_command_handlers.mjs +52 -0
  51. package/dist/src/cli/arguments.d.mts +38 -0
  52. package/dist/src/cli/arguments.mjs +130 -0
  53. package/dist/src/cli/basic_command_handlers.d.mts +4 -0
  54. package/dist/src/cli/basic_command_handlers.mjs +57 -0
  55. package/dist/src/cli/cli_commands.d.mts +14 -0
  56. package/dist/src/cli/cli_commands.mjs +34 -0
  57. package/dist/src/cli/command_contract.d.mts +57 -0
  58. package/dist/src/cli/command_contract.mjs +39 -0
  59. package/dist/src/cli/command_output.d.mts +12 -0
  60. package/dist/src/cli/command_output.mjs +20 -0
  61. package/dist/src/cli/command_registry.d.mts +7 -0
  62. package/dist/src/cli/command_registry.mjs +14 -0
  63. package/dist/src/cli/guided.d.mts +53 -0
  64. package/dist/src/cli/guided.mjs +727 -0
  65. package/dist/src/cli/identity_command_handlers.d.mts +12 -0
  66. package/dist/src/cli/identity_command_handlers.mjs +239 -0
  67. package/dist/src/cli/identity_renderer.d.mts +8 -0
  68. package/dist/src/cli/identity_renderer.mjs +33 -0
  69. package/dist/src/cli/init_command.d.mts +21 -0
  70. package/dist/src/cli/init_command.mjs +90 -0
  71. package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
  72. package/dist/src/cli/inspection_command_handlers.mjs +79 -0
  73. package/dist/src/cli/project_command_handlers.d.mts +11 -0
  74. package/dist/src/cli/project_command_handlers.mjs +92 -0
  75. package/dist/src/cli/project_commands.d.mts +283 -0
  76. package/dist/src/cli/project_commands.mjs +192 -0
  77. package/dist/src/cli/rehearsal.d.mts +7 -0
  78. package/dist/src/cli/rehearsal.mjs +190 -0
  79. package/dist/src/cli/renderers.d.mts +299 -0
  80. package/dist/src/cli/renderers.mjs +554 -0
  81. package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
  82. package/dist/src/cli/runtime_action_handlers.mjs +201 -0
  83. package/dist/src/cli/runtime_commands.d.mts +294 -0
  84. package/dist/src/cli/runtime_commands.mjs +619 -0
  85. package/dist/src/cli/source_command_handlers.d.mts +8 -0
  86. package/dist/src/cli/source_command_handlers.mjs +62 -0
  87. package/dist/src/cli/source_commands.d.mts +436 -0
  88. package/dist/src/cli/source_commands.mjs +301 -0
  89. package/dist/src/cli/terminal.d.mts +59 -0
  90. package/dist/src/cli/terminal.mjs +230 -0
  91. package/dist/src/identity/claim.d.mts +5 -0
  92. package/dist/src/identity/claim.mjs +4 -0
  93. package/dist/src/identity/claim_contract.d.mts +135 -0
  94. package/dist/src/identity/claim_contract.mjs +6 -0
  95. package/dist/src/identity/claim_executor.d.mts +10 -0
  96. package/dist/src/identity/claim_executor.mjs +358 -0
  97. package/dist/src/identity/claim_identity_lookup.d.mts +23 -0
  98. package/dist/src/identity/claim_identity_lookup.mjs +134 -0
  99. package/dist/src/identity/claim_plan.d.mts +8 -0
  100. package/dist/src/identity/claim_plan.mjs +61 -0
  101. package/dist/src/identity/claim_policy.d.mts +3 -0
  102. package/dist/src/identity/claim_policy.mjs +377 -0
  103. package/dist/src/identity/claim_sql.d.mts +10 -0
  104. package/dist/src/identity/claim_sql.mjs +46 -0
  105. package/dist/src/identity/claim_verification.d.mts +13 -0
  106. package/dist/src/identity/claim_verification.mjs +35 -0
  107. package/dist/src/identity/connect.d.mts +30 -0
  108. package/dist/src/identity/connect.mjs +111 -0
  109. package/dist/src/identity/provider_configuration.d.mts +37 -0
  110. package/dist/src/identity/provider_configuration.mjs +132 -0
  111. package/dist/src/identity/storage.d.mts +21 -0
  112. package/dist/src/identity/storage.mjs +122 -0
  113. package/dist/src/index.d.mts +4 -0
  114. package/dist/src/index.mjs +3 -0
  115. package/dist/src/project/configuration.d.mts +13 -0
  116. package/dist/src/project/configuration.mjs +9 -0
  117. package/dist/src/project/configuration_contract.d.mts +125 -0
  118. package/dist/src/project/configuration_contract.mjs +27 -0
  119. package/dist/src/project/configuration_discovery.d.mts +7 -0
  120. package/dist/src/project/configuration_discovery.mjs +37 -0
  121. package/dist/src/project/configuration_normalization.d.mts +107 -0
  122. package/dist/src/project/configuration_normalization.mjs +302 -0
  123. package/dist/src/project/configuration_rendering.d.mts +14 -0
  124. package/dist/src/project/configuration_rendering.mjs +228 -0
  125. package/dist/src/project/configuration_resolution.d.mts +125 -0
  126. package/dist/src/project/configuration_resolution.mjs +81 -0
  127. package/dist/src/project/configuration_schema.d.mts +22 -0
  128. package/dist/src/project/configuration_schema.mjs +160 -0
  129. package/dist/src/project/configuration_validation.d.mts +22 -0
  130. package/dist/src/project/configuration_validation.mjs +136 -0
  131. package/dist/src/project/install_scaffold.d.mts +42 -0
  132. package/dist/src/project/install_scaffold.mjs +263 -0
  133. package/dist/src/project/project_detection.d.mts +14 -0
  134. package/dist/src/project/project_detection.mjs +47 -0
  135. package/dist/src/project/setup.d.mts +87 -0
  136. package/dist/src/project/setup.mjs +414 -0
  137. package/dist/src/project/support_report.d.mts +88 -0
  138. package/dist/src/project/support_report.mjs +83 -0
  139. package/dist/src/runtime/cleanup.d.mts +85 -0
  140. package/dist/src/runtime/cleanup.mjs +311 -0
  141. package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
  142. package/dist/src/runtime/lifecycle_engine.mjs +114 -0
  143. package/dist/src/runtime/migration_history.d.mts +51 -0
  144. package/dist/src/runtime/migration_history.mjs +170 -0
  145. package/dist/src/runtime/plan.d.mts +134 -0
  146. package/dist/src/runtime/plan.mjs +640 -0
  147. package/dist/src/runtime/policy.d.mts +66 -0
  148. package/dist/src/runtime/policy.mjs +369 -0
  149. package/dist/src/runtime/restore.d.mts +22 -0
  150. package/dist/src/runtime/restore.mjs +276 -0
  151. package/dist/src/runtime/service_environment.d.mts +11 -0
  152. package/dist/src/runtime/service_environment.mjs +58 -0
  153. package/dist/src/runtime/topology.d.mts +51 -0
  154. package/dist/src/runtime/topology.mjs +128 -0
  155. package/dist/src/shared/diagnostics.d.mts +97 -0
  156. package/dist/src/shared/diagnostics.mjs +170 -0
  157. package/dist/src/shared/human_output.d.mts +2 -0
  158. package/{src → dist/src}/shared/human_output.mjs +1 -3
  159. package/dist/src/shared/node_runtime.d.mts +14 -0
  160. package/dist/src/shared/node_runtime.mjs +25 -0
  161. package/dist/src/shared/operation_guard.d.mts +22 -0
  162. package/dist/src/shared/operation_guard.mjs +150 -0
  163. package/dist/src/shared/process_environment.d.mts +15 -0
  164. package/dist/src/shared/process_environment.mjs +50 -0
  165. package/dist/src/source/access.d.mts +165 -0
  166. package/dist/src/source/access.mjs +602 -0
  167. package/dist/src/source/asset_transfer.d.mts +27 -0
  168. package/dist/src/source/asset_transfer.mjs +202 -0
  169. package/dist/src/source/baseline.d.mts +66 -0
  170. package/dist/src/source/baseline.mjs +372 -0
  171. package/dist/src/source/external_reader.d.mts +42 -0
  172. package/dist/src/source/external_reader.mjs +278 -0
  173. package/dist/src/source/postgresql_access.d.mts +86 -0
  174. package/dist/src/source/postgresql_access.mjs +454 -0
  175. package/dist/src/source/privacy_paths.d.mts +10 -0
  176. package/dist/src/source/privacy_paths.mjs +27 -0
  177. package/dist/src/targets/driver.d.mts +2 -0
  178. package/dist/src/targets/driver.mjs +13 -0
  179. package/dist/src/targets/postgresql.d.mts +5 -0
  180. package/dist/src/targets/postgresql.mjs +620 -0
  181. package/dist/src/targets/supabase.d.mts +8 -0
  182. package/dist/src/targets/supabase.mjs +629 -0
  183. package/dist/src/targets/supabase_environment.d.mts +54 -0
  184. package/dist/src/targets/supabase_environment.mjs +184 -0
  185. package/dist/src/targets/target.d.mts +33 -0
  186. package/dist/src/targets/target.mjs +39 -0
  187. package/docs/README.md +35 -16
  188. package/docs/architecture.md +135 -10
  189. package/docs/commands.md +53 -15
  190. package/docs/configuration.md +186 -31
  191. package/docs/getting-started.md +66 -9
  192. package/docs/index.md +67 -0
  193. package/docs/production-source.md +175 -17
  194. package/docs/releasing.md +24 -12
  195. package/docs/roadmap.md +3 -2
  196. package/docs/runtime-lifecycle.md +99 -0
  197. package/docs/runtime-policies.md +80 -7
  198. package/docs/sanitization.md +572 -4
  199. package/docs/security-model.md +8 -0
  200. package/docs/stability.md +70 -0
  201. package/docs/standalone-workflow.md +36 -13
  202. package/docs/troubleshooting.md +82 -10
  203. package/docs/tutorial.md +7 -1
  204. package/docs/typescript.md +134 -0
  205. package/package.json +44 -29
  206. package/types/configuration.d.mts +5 -0
  207. package/src/README.md +0 -17
  208. package/src/application/session.mjs +0 -313
  209. package/src/baseline/artifact.mjs +0 -661
  210. package/src/baseline/builder.mjs +0 -164
  211. package/src/baseline/input_discovery.mjs +0 -155
  212. package/src/baseline/policy_review.mjs +0 -92
  213. package/src/baseline/preparation.mjs +0 -323
  214. package/src/baseline/privacy_engine.mjs +0 -413
  215. package/src/baseline/sanitization_policy.mjs +0 -299
  216. package/src/baseline/schema_snapshot.mjs +0 -113
  217. package/src/cli/arguments.mjs +0 -120
  218. package/src/cli/guided.mjs +0 -807
  219. package/src/cli/rehearsal.mjs +0 -933
  220. package/src/cli/renderers.mjs +0 -584
  221. package/src/cli/runtime_commands.mjs +0 -553
  222. package/src/cli/source_commands.mjs +0 -326
  223. package/src/cli/terminal.mjs +0 -275
  224. package/src/identity/claim.mjs +0 -975
  225. package/src/identity/storage.mjs +0 -165
  226. package/src/project/configuration.d.mts +0 -153
  227. package/src/project/configuration.mjs +0 -1197
  228. package/src/project/setup.mjs +0 -379
  229. package/src/project/support_report.mjs +0 -105
  230. package/src/runtime/cleanup.mjs +0 -381
  231. package/src/runtime/migration_history.mjs +0 -220
  232. package/src/runtime/plan.mjs +0 -685
  233. package/src/runtime/policy.mjs +0 -438
  234. package/src/runtime/restore.mjs +0 -329
  235. package/src/runtime/service_environment.mjs +0 -82
  236. package/src/runtime/topology.mjs +0 -177
  237. package/src/shared/diagnostics.mjs +0 -199
  238. package/src/shared/operation_guard.mjs +0 -162
  239. package/src/shared/process_environment.mjs +0 -68
  240. package/src/source/access.mjs +0 -578
  241. package/src/source/asset_transfer.mjs +0 -177
  242. package/src/source/baseline.mjs +0 -446
  243. package/src/source/postgresql_access.mjs +0 -480
  244. package/src/targets/postgresql.mjs +0 -808
  245. package/src/targets/supabase.mjs +0 -830
  246. package/src/targets/supabase_environment.mjs +0 -229
  247. package/src/targets/target.mjs +0 -66
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Build a synthetic baseline from explicit project-owned local files. This module does
3
+ * not connect to a hosted source or decide which values are safe.
4
+ */
5
+ import { createHash } from "node:crypto";
6
+ import { createReadStream } from "node:fs";
7
+ import { readFile } from "node:fs/promises";
8
+ import { isAbsolute, relative, resolve, sep } from "node:path";
9
+ import { createInterface } from "node:readline";
10
+ import { pathToFileURL } from "node:url";
11
+ import { createAndActivateBaseline, createBaselineGenerationId, } from "./artifact.mjs";
12
+ import { loadRehearsalConfig } from "../project/configuration.mjs";
13
+ import { buildMigrationLedgerInventory, createMigrationReplayReceipt, readMigrationSourceBundle, } from "../runtime/migration_history.mjs";
14
+ import { validateRuntimeSanitizationPolicy } from "./sanitization_policy.mjs";
15
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
16
+ const parseAssets = (value) => {
17
+ if (!Array.isArray(value)) {
18
+ throw new Error("Synthetic assets manifest must be an array.");
19
+ }
20
+ return value.map((entry, index) => {
21
+ if (!isObject(entry) ||
22
+ typeof entry.bucket !== "string" ||
23
+ typeof entry.objectPath !== "string" ||
24
+ typeof entry.file !== "string" ||
25
+ (entry.contentType !== undefined && typeof entry.contentType !== "string")) {
26
+ throw new Error(`Synthetic asset ${index} has an invalid shape.`);
27
+ }
28
+ return {
29
+ bucket: entry.bucket,
30
+ objectPath: entry.objectPath,
31
+ file: entry.file,
32
+ ...(entry.contentType === undefined
33
+ ? {}
34
+ : { contentType: entry.contentType }),
35
+ };
36
+ });
37
+ };
38
+ const resolveProjectInput = (projectRoot, value, label) => {
39
+ if (typeof value !== "string" || value.trim() === "") {
40
+ throw new Error(`${label} is required.`);
41
+ }
42
+ const path = resolve(projectRoot, value);
43
+ const owned = relative(projectRoot, path);
44
+ if (owned === "" ||
45
+ owned === ".." ||
46
+ owned.startsWith(`..${sep}`) ||
47
+ isAbsolute(owned)) {
48
+ throw new Error(`${label} must be a file inside the project root.`);
49
+ }
50
+ return path;
51
+ };
52
+ const readRecords = async function* (path) {
53
+ const lines = createInterface({
54
+ input: createReadStream(path, { encoding: "utf8" }),
55
+ crlfDelay: Infinity,
56
+ });
57
+ for await (const line of lines) {
58
+ if (line.trim() === "")
59
+ continue;
60
+ let record;
61
+ try {
62
+ record = JSON.parse(line);
63
+ }
64
+ catch (error) {
65
+ throw new Error("Synthetic baseline input contains invalid NDJSON.", {
66
+ cause: error,
67
+ });
68
+ }
69
+ yield record;
70
+ }
71
+ };
72
+ export const createSyntheticBaselineFromFiles = async ({ projectRoot = process.cwd(), configPath, recordsPath, ledgerPath, assetsPath, }) => {
73
+ const loaded = await loadRehearsalConfig({
74
+ projectRoot,
75
+ ...(configPath === undefined ? {} : { configPath }),
76
+ });
77
+ const records = resolveProjectInput(loaded.projectRoot, recordsPath, "Synthetic records path");
78
+ const ledger = resolveProjectInput(loaded.projectRoot, ledgerPath, "Migration ledger path");
79
+ const [policyBytes, ledgerBytes] = await Promise.all([
80
+ readFile(loaded.paths.sanitizationPolicy),
81
+ readFile(ledger),
82
+ ]);
83
+ const assets = assetsPath
84
+ ? await Promise.all(parseAssets(JSON.parse(await readFile(resolveProjectInput(loaded.projectRoot, assetsPath, "Synthetic assets manifest path"), "utf8"))).map(async (asset) => ({
85
+ bucket: asset.bucket,
86
+ objectPath: asset.objectPath,
87
+ ...(asset.contentType === undefined
88
+ ? {}
89
+ : { contentType: asset.contentType }),
90
+ content: await readFile(resolveProjectInput(loaded.projectRoot, asset.file, "Synthetic asset file path")),
91
+ })))
92
+ : [];
93
+ const policy = validateRuntimeSanitizationPolicy(JSON.parse(policyBytes.toString("utf8")));
94
+ const expectedTables = policy.tables
95
+ .filter((table) => table.sourceRows !== "EXCLUDE")
96
+ .map((table) => (table.schema ?? "public") === "public"
97
+ ? table.name
98
+ : `${table.schema}.${table.name}`);
99
+ if (expectedTables.length === 0) {
100
+ throw new Error("The sanitization policy includes no restorable tables.");
101
+ }
102
+ const sourceMigrationHistory = buildMigrationLedgerInventory(JSON.parse(ledgerBytes.toString("utf8")), "Synthetic migration ledger");
103
+ const migrationCutoff = sourceMigrationHistory.at(-1);
104
+ if (migrationCutoff === undefined) {
105
+ throw new Error("The synthetic migration ledger is empty.");
106
+ }
107
+ if (policy.migrationCutoff !== migrationCutoff.version) {
108
+ throw new Error(`The sanitization policy cutoff ${policy.migrationCutoff} does not match migration ledger cutoff ${migrationCutoff.version}.`);
109
+ }
110
+ const migrationFiles = await readMigrationSourceBundle({
111
+ directory: new URL("./", pathToFileURL(`${loaded.paths.migrationDirectory}${sep}`)),
112
+ sourceLedger: sourceMigrationHistory,
113
+ });
114
+ const migrationReceipt = createMigrationReplayReceipt({
115
+ files: migrationFiles,
116
+ ledger: sourceMigrationHistory,
117
+ });
118
+ const generationId = createBaselineGenerationId();
119
+ const baseline = await createAndActivateBaseline({
120
+ artifactRoot: loaded.paths.artifactDirectory,
121
+ generationId,
122
+ records: readRecords(records),
123
+ metadata: {
124
+ migrationCutoff: migrationCutoff.version,
125
+ migrationHistorySha256: migrationReceipt.historySha256,
126
+ sanitizationPolicySha256: createHash("sha256")
127
+ .update(policyBytes)
128
+ .digest("hex"),
129
+ sourceMigrationHistory,
130
+ },
131
+ expectedTables,
132
+ migrationFiles,
133
+ assets,
134
+ });
135
+ return {
136
+ generationId,
137
+ migrationCutoff: baseline.migrationCutoff,
138
+ migrationCount: Object.keys(baseline.migrations).length,
139
+ rowCount: baseline.rowCount,
140
+ tableCount: Object.keys(baseline.tableCounts).length,
141
+ };
142
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Discover likely project-owned baseline inputs without exposing their values.
3
+ * Discovery is bounded, ignores generated/private directories, and only sniffs
4
+ * small JSON or NDJSON files before returning project-relative paths.
5
+ */
6
+ export interface DiscoveredBaselineInputs {
7
+ records: string[];
8
+ ledgers: string[];
9
+ assetManifests: string[];
10
+ }
11
+ export declare const discoverBaselineInputFiles: ({ projectRoot, }?: {
12
+ projectRoot?: string;
13
+ }) => Promise<DiscoveredBaselineInputs>;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Discover likely project-owned baseline inputs without exposing their values.
3
+ * Discovery is bounded, ignores generated/private directories, and only sniffs
4
+ * small JSON or NDJSON files before returning project-relative paths.
5
+ */
6
+ import { open, readFile, readdir, stat } from "node:fs/promises";
7
+ import { extname, join, relative, sep } from "node:path";
8
+ const ignoredDirectories = new Set([
9
+ ".git",
10
+ ".next",
11
+ ".rehearsal",
12
+ ".turbo",
13
+ "build",
14
+ "coverage",
15
+ "dist",
16
+ "node_modules",
17
+ ]);
18
+ const maximumDepth = 4;
19
+ const maximumFiles = 2_000;
20
+ const maximumJsonBytes = 2 * 1024 * 1024;
21
+ const maximumNdjsonProbeBytes = 64 * 1024;
22
+ const projectRelativePath = (projectRoot, path) => relative(projectRoot, path).split(sep).join("/");
23
+ const preference = {
24
+ records: [
25
+ "rehearsal/sanitized-data.ndjson",
26
+ "rehearsal/synthetic-data.ndjson",
27
+ ],
28
+ ledgers: ["rehearsal/migration-ledger.json"],
29
+ assetManifests: ["rehearsal/assets.json"],
30
+ };
31
+ const sortCandidates = (kind, candidates) => {
32
+ const preferred = new Map(preference[kind].map((path, index) => [path, index]));
33
+ return candidates.sort((left, right) => {
34
+ const leftRank = preferred.get(left) ?? Number.POSITIVE_INFINITY;
35
+ const rightRank = preferred.get(right) ?? Number.POSITIVE_INFINITY;
36
+ return leftRank - rightRank || left.localeCompare(right);
37
+ });
38
+ };
39
+ const readFirstRecord = async (path) => {
40
+ const handle = await open(path, "r");
41
+ try {
42
+ const buffer = Buffer.alloc(maximumNdjsonProbeBytes);
43
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
44
+ const probe = buffer.subarray(0, bytesRead).toString("utf8");
45
+ const lines = probe.split(/\r?\n/u);
46
+ for (const line of lines) {
47
+ if (!line.trim())
48
+ continue;
49
+ return JSON.parse(line);
50
+ }
51
+ }
52
+ finally {
53
+ await handle.close();
54
+ }
55
+ return undefined;
56
+ };
57
+ const isRecord = (value) => Boolean(value &&
58
+ typeof value === "object" &&
59
+ !Array.isArray(value) &&
60
+ "table" in value &&
61
+ typeof value.table === "string" &&
62
+ "row" in value &&
63
+ value.row &&
64
+ typeof value.row === "object" &&
65
+ !Array.isArray(value.row));
66
+ const isMigrationLedger = (value) => Array.isArray(value) &&
67
+ value.length > 0 &&
68
+ value.every((entry) => entry &&
69
+ typeof entry === "object" &&
70
+ "version" in entry &&
71
+ typeof entry.version === "string" &&
72
+ "name" in entry &&
73
+ typeof entry.name === "string" &&
74
+ "statements" in entry &&
75
+ Array.isArray(entry.statements));
76
+ const isAssetManifest = (value) => Array.isArray(value) &&
77
+ value.length > 0 &&
78
+ value.every((entry) => entry &&
79
+ typeof entry === "object" &&
80
+ "bucket" in entry &&
81
+ typeof entry.bucket === "string" &&
82
+ "objectPath" in entry &&
83
+ typeof entry.objectPath === "string" &&
84
+ "file" in entry &&
85
+ typeof entry.file === "string");
86
+ const collectCandidateFiles = async (projectRoot) => {
87
+ const files = [];
88
+ const visit = async (directory, depth) => {
89
+ if (depth > maximumDepth || files.length >= maximumFiles)
90
+ return;
91
+ const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
92
+ entries.sort((left, right) => left.name.localeCompare(right.name));
93
+ for (const entry of entries) {
94
+ if (files.length >= maximumFiles)
95
+ return;
96
+ if (entry.isSymbolicLink())
97
+ continue;
98
+ const path = join(directory, entry.name);
99
+ if (entry.isDirectory()) {
100
+ if (!ignoredDirectories.has(entry.name))
101
+ await visit(path, depth + 1);
102
+ continue;
103
+ }
104
+ if (!entry.isFile() || ![".json", ".ndjson"].includes(extname(path))) {
105
+ continue;
106
+ }
107
+ files.push(path);
108
+ }
109
+ };
110
+ await visit(projectRoot, 0);
111
+ return files;
112
+ };
113
+ export const discoverBaselineInputFiles = async ({ projectRoot = process.cwd(), } = {}) => {
114
+ const result = {
115
+ records: [],
116
+ ledgers: [],
117
+ assetManifests: [],
118
+ };
119
+ for (const path of await collectCandidateFiles(projectRoot)) {
120
+ const pathStats = await stat(path).catch(() => undefined);
121
+ if (!pathStats?.isFile())
122
+ continue;
123
+ const relativePath = projectRelativePath(projectRoot, path);
124
+ try {
125
+ if (extname(path) === ".ndjson") {
126
+ if (isRecord(await readFirstRecord(path))) {
127
+ result.records.push(relativePath);
128
+ }
129
+ continue;
130
+ }
131
+ if (pathStats.size > maximumJsonBytes)
132
+ continue;
133
+ const value = JSON.parse(await readFile(path, "utf8"));
134
+ if (isMigrationLedger(value))
135
+ result.ledgers.push(relativePath);
136
+ else if (isAssetManifest(value))
137
+ result.assetManifests.push(relativePath);
138
+ }
139
+ catch {
140
+ // Invalid or unreadable files are not candidates; explicit validation will
141
+ // provide actionable errors if the user enters one manually.
142
+ }
143
+ }
144
+ return {
145
+ records: sortCandidates("records", result.records),
146
+ ledgers: sortCandidates("ledgers", result.ledgers),
147
+ assetManifests: sortCandidates("assetManifests", result.assetManifests),
148
+ };
149
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Purpose: Complete and safely apply a generated REVIEW REQUIRED policy draft.
3
+ * The caller owns the interactive presentation; this module owns validation and
4
+ * concurrent-change refusal.
5
+ */
6
+ export declare const SAFE_COLUMN_PRESET: Readonly<{
7
+ action: "REPLACE WITH SYNTHETIC";
8
+ generated: "NEVER";
9
+ identity: "NO";
10
+ foreignKey: null;
11
+ }>;
12
+ type ColumnReference = string | {
13
+ name: string;
14
+ };
15
+ type ReviewedColumn = Record<string, unknown>;
16
+ interface PolicyColumn extends Record<string, unknown> {
17
+ name: string;
18
+ }
19
+ interface PolicyTable extends Record<string, unknown> {
20
+ name: string;
21
+ columns: PolicyColumn[];
22
+ }
23
+ interface PolicyDraft extends Record<string, unknown> {
24
+ draft: true;
25
+ tables: PolicyTable[];
26
+ }
27
+ export declare const createSafeTablePreset: (columns: readonly ColumnReference[]) => {
28
+ [k: string]: {
29
+ action: "REPLACE WITH SYNTHETIC";
30
+ generated: "NEVER";
31
+ identity: "NO";
32
+ foreignKey: null;
33
+ };
34
+ };
35
+ export declare const suggestPolicyExceptionColumns: (columns: readonly ColumnReference[]) => string[];
36
+ export declare const readReviewablePolicyDraft: (path: string) => Promise<{
37
+ source: string;
38
+ draft: PolicyDraft;
39
+ }>;
40
+ export declare const completePolicyDraft: ({ draft, reviewColumn, reviewTable, }: {
41
+ draft: unknown;
42
+ reviewColumn?: (input: {
43
+ table: string;
44
+ column: string;
45
+ }) => ReviewedColumn | undefined | Promise<ReviewedColumn | undefined>;
46
+ reviewTable?: (input: {
47
+ table: string;
48
+ columns: string[];
49
+ }) => Record<string, ReviewedColumn> | undefined | Promise<Record<string, ReviewedColumn> | undefined>;
50
+ }) => Promise<unknown>;
51
+ export declare const applyReviewedPolicy: ({ path, originalSource, policy, }: {
52
+ path: string;
53
+ originalSource: string;
54
+ policy: unknown;
55
+ }) => Promise<void>;
56
+ export {};
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Purpose: Complete and safely apply a generated REVIEW REQUIRED policy draft.
3
+ * The caller owns the interactive presentation; this module owns validation and
4
+ * concurrent-change refusal.
5
+ */
6
+ import { readFile, writeFile } from "node:fs/promises";
7
+ import { validateRuntimeSanitizationPolicy } from "./sanitization_policy.mjs";
8
+ export const SAFE_COLUMN_PRESET = Object.freeze({
9
+ action: "REPLACE WITH SYNTHETIC",
10
+ generated: "NEVER",
11
+ identity: "NO",
12
+ foreignKey: null,
13
+ });
14
+ const isPolicyDraft = (value) => typeof value === "object" &&
15
+ value !== null &&
16
+ "draft" in value &&
17
+ value.draft === true &&
18
+ "tables" in value &&
19
+ Array.isArray(value.tables);
20
+ export const createSafeTablePreset = (columns) => Object.fromEntries(columns.map((column) => [
21
+ typeof column === "string" ? column : column.name,
22
+ { ...SAFE_COLUMN_PRESET },
23
+ ]));
24
+ export const suggestPolicyExceptionColumns = (columns) => columns
25
+ .map((column) => (typeof column === "string" ? column : column.name))
26
+ .filter((column) => column === "id" ||
27
+ column.endsWith("_id") ||
28
+ ["created_at", "updated_at", "deleted_at"].includes(column));
29
+ export const readReviewablePolicyDraft = async (path) => {
30
+ const source = await readFile(path, "utf8");
31
+ const draft = JSON.parse(source);
32
+ if (!isPolicyDraft(draft)) {
33
+ throw new Error("Interactive review requires a generated sanitization policy with draft: true.");
34
+ }
35
+ return { source, draft };
36
+ };
37
+ export const completePolicyDraft = async ({ draft, reviewColumn, reviewTable, }) => {
38
+ if (!isPolicyDraft(draft)) {
39
+ throw new Error("A generated REVIEW REQUIRED policy draft is required.");
40
+ }
41
+ const tables = [];
42
+ for (const table of draft.tables) {
43
+ const columnNames = (table.columns ?? []).map((column) => column.name);
44
+ const tableDecisions = reviewTable
45
+ ? await reviewTable({ table: table.name, columns: columnNames })
46
+ : {};
47
+ const columns = [];
48
+ for (const column of table.columns ?? []) {
49
+ const reviewed = tableDecisions?.[column.name] ??
50
+ (await reviewColumn?.({
51
+ table: table.name,
52
+ column: column.name,
53
+ }));
54
+ if (!reviewed) {
55
+ throw new Error(`Policy review did not classify ${table.name}.${column.name}.`);
56
+ }
57
+ columns.push({ name: column.name, ...reviewed });
58
+ }
59
+ tables.push({ ...table, columns });
60
+ }
61
+ const reviewed = { ...draft, tables };
62
+ delete reviewed.draft;
63
+ return validateRuntimeSanitizationPolicy(reviewed);
64
+ };
65
+ export const applyReviewedPolicy = async ({ path, originalSource, policy, }) => {
66
+ validateRuntimeSanitizationPolicy(policy);
67
+ if ((await readFile(path, "utf8")) !== originalSource) {
68
+ throw new Error("The sanitization policy changed during review; no reviewed policy was written.");
69
+ }
70
+ await writeFile(path, `${JSON.stringify(policy, null, 2)}\n`, {
71
+ mode: 0o600,
72
+ });
73
+ };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Purpose: Inspect explicit local synthetic records and migration evidence, then
3
+ * create a fail-closed sanitization-policy draft for human review.
4
+ */
5
+ import { type MigrationLedgerEntry } from "../runtime/migration_history.mjs";
6
+ export interface SyntheticTableInspection {
7
+ readonly schema: string;
8
+ readonly name: string;
9
+ readonly rowCount: number;
10
+ readonly columns: string[];
11
+ }
12
+ interface SyntheticRecordInspection {
13
+ readonly rowCount: number;
14
+ readonly tables: SyntheticTableInspection[];
15
+ }
16
+ export interface BaselineInputInspection extends SyntheticRecordInspection {
17
+ readonly projectRoot: string;
18
+ readonly recordsPath: string;
19
+ readonly ledgerPath: string;
20
+ readonly assetsPath?: string;
21
+ readonly migrationHistory: MigrationLedgerEntry[];
22
+ readonly migrationCutoff: string;
23
+ readonly migrationCount: number;
24
+ readonly assetCount: number;
25
+ }
26
+ export interface BaselinePreparationPlan extends SyntheticRecordInspection {
27
+ readonly projectRoot: string;
28
+ readonly destination: string;
29
+ readonly destinationRelative: string;
30
+ readonly recordsPath: string;
31
+ readonly ledgerPath: string;
32
+ readonly migrationCutoff: string;
33
+ readonly migrationCount: number;
34
+ readonly content: string;
35
+ }
36
+ export declare const inspectBaselineInputFiles: ({ projectRoot, configPath, recordsPath, ledgerPath, assetsPath, }: {
37
+ projectRoot?: string;
38
+ configPath?: string;
39
+ recordsPath: string;
40
+ ledgerPath: string;
41
+ assetsPath?: string;
42
+ }) => Promise<BaselineInputInspection>;
43
+ export declare const planBaselinePreparation: ({ projectRoot, configPath, recordsPath, ledgerPath, }: {
44
+ projectRoot?: string;
45
+ configPath?: string;
46
+ recordsPath: string;
47
+ ledgerPath: string;
48
+ }) => Promise<BaselinePreparationPlan>;
49
+ export declare const applyBaselinePreparation: (plan: BaselinePreparationPlan) => Promise<void>;
50
+ export declare const summarizeBaselinePreparation: (plan: BaselinePreparationPlan, { mode }: {
51
+ mode: "preview" | "written";
52
+ }) => {
53
+ mode: "preview" | "written";
54
+ destination: string;
55
+ recordsPath: string;
56
+ ledgerPath: string;
57
+ migrationCutoff: string;
58
+ migrationCount: number;
59
+ rowCount: number;
60
+ tables: readonly SyntheticTableInspection[];
61
+ nextAction: string;
62
+ };
63
+ export {};