@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,295 @@
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 { createReadStream } from "node:fs";
6
+ import { access, lstat, mkdir, open, readFile } from "node:fs/promises";
7
+ import { createInterface } from "node:readline";
8
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
9
+ import { loadRehearsalConfig } from "../project/configuration.mjs";
10
+ import { buildMigrationLedgerInventory, } from "../runtime/migration_history.mjs";
11
+ const identifierPattern = /^[a-z][a-z0-9_]{0,62}$/u;
12
+ const storageBucketPattern = /^[a-z0-9][a-z0-9.-]{0,99}$/u;
13
+ const hasErrorCode = (error, code) => typeof error === "object" &&
14
+ error !== null &&
15
+ "code" in error &&
16
+ error.code === code;
17
+ const resolveProjectInput = (projectRoot, value, label) => {
18
+ if (typeof value !== "string" || value.trim() === "") {
19
+ throw new Error(`${label} is required.`);
20
+ }
21
+ const path = resolve(projectRoot, value);
22
+ const owned = relative(projectRoot, path);
23
+ if (owned === "" ||
24
+ owned === ".." ||
25
+ owned.startsWith(`..${sep}`) ||
26
+ isAbsolute(owned)) {
27
+ throw new Error(`${label} must be a file inside the project root.`);
28
+ }
29
+ return path;
30
+ };
31
+ const pathExists = (path) => access(path)
32
+ .then(() => true)
33
+ .catch((error) => {
34
+ if (hasErrorCode(error, "ENOENT"))
35
+ return false;
36
+ throw error;
37
+ });
38
+ const assertRegularInput = async (path, label) => {
39
+ let stats;
40
+ try {
41
+ stats = await lstat(path);
42
+ }
43
+ catch (error) {
44
+ if (hasErrorCode(error, "ENOENT"))
45
+ throw new Error(`${label} does not exist.`);
46
+ throw error;
47
+ }
48
+ if (stats.isSymbolicLink() || !stats.isFile()) {
49
+ throw new Error(`${label} must be a regular project-local file.`);
50
+ }
51
+ };
52
+ const inspectSyntheticRecords = async (path) => {
53
+ const tables = new Map();
54
+ let rowCount = 0;
55
+ const lines = createInterface({
56
+ input: createReadStream(path, { encoding: "utf8" }),
57
+ crlfDelay: Infinity,
58
+ });
59
+ for await (const line of lines) {
60
+ if (!line.trim())
61
+ continue;
62
+ let record;
63
+ try {
64
+ record = JSON.parse(line);
65
+ }
66
+ catch (error) {
67
+ throw new Error("Synthetic baseline input contains invalid NDJSON.", {
68
+ cause: error,
69
+ });
70
+ }
71
+ if (typeof record !== "object" ||
72
+ record === null ||
73
+ Array.isArray(record) ||
74
+ !("table" in record) ||
75
+ typeof record.table !== "string" ||
76
+ !("row" in record) ||
77
+ typeof record.row !== "object" ||
78
+ record.row === null ||
79
+ Array.isArray(record.row) ||
80
+ ("schema" in record &&
81
+ record.schema !== undefined &&
82
+ typeof record.schema !== "string")) {
83
+ throw new Error("A synthetic baseline record has an invalid shape.");
84
+ }
85
+ const schema = "schema" in record && typeof record.schema === "string"
86
+ ? record.schema
87
+ : "public";
88
+ if (!identifierPattern.test(schema) ||
89
+ !identifierPattern.test(record.table)) {
90
+ throw new Error("A synthetic baseline record has an invalid shape.");
91
+ }
92
+ const columns = Object.keys(record.row);
93
+ if (columns.length === 0 ||
94
+ columns.some((column) => !identifierPattern.test(column))) {
95
+ throw new Error(`Synthetic baseline table ${record.table} has invalid or empty columns.`);
96
+ }
97
+ const key = schema === "public" ? record.table : `${schema}.${record.table}`;
98
+ const table = tables.get(key) ?? {
99
+ schema,
100
+ name: record.table,
101
+ rowCount: 0,
102
+ columns: new Set(),
103
+ };
104
+ table.rowCount += 1;
105
+ for (const column of columns)
106
+ table.columns.add(column);
107
+ tables.set(key, table);
108
+ rowCount += 1;
109
+ }
110
+ if (rowCount === 0) {
111
+ throw new Error("Synthetic baseline input contains no records.");
112
+ }
113
+ return {
114
+ rowCount,
115
+ tables: [...tables.values()]
116
+ .sort((left, right) => left.name.localeCompare(right.name))
117
+ .map((table) => ({
118
+ schema: table.schema,
119
+ name: table.name,
120
+ rowCount: table.rowCount,
121
+ columns: [...table.columns].sort(),
122
+ })),
123
+ };
124
+ };
125
+ const inspectAssetManifest = async (path, projectRoot) => {
126
+ let manifest;
127
+ try {
128
+ manifest = JSON.parse(await readFile(path, "utf8"));
129
+ }
130
+ catch (error) {
131
+ throw new Error("Storage asset manifest contains invalid JSON.", {
132
+ cause: error,
133
+ });
134
+ }
135
+ if (!Array.isArray(manifest)) {
136
+ throw new Error("Storage asset manifest must be a JSON array.");
137
+ }
138
+ const destinations = new Set();
139
+ for (const [index, asset] of manifest.entries()) {
140
+ if (typeof asset !== "object" ||
141
+ asset === null ||
142
+ Array.isArray(asset) ||
143
+ !("bucket" in asset) ||
144
+ typeof asset.bucket !== "string" ||
145
+ !storageBucketPattern.test(asset.bucket) ||
146
+ !("objectPath" in asset) ||
147
+ typeof asset.objectPath !== "string" ||
148
+ !asset.objectPath ||
149
+ asset.objectPath
150
+ .split("/")
151
+ .some((segment) => !segment || segment === "." || segment === "..") ||
152
+ !("file" in asset) ||
153
+ typeof asset.file !== "string" ||
154
+ !asset.file.trim() ||
155
+ ("contentType" in asset &&
156
+ asset.contentType !== undefined &&
157
+ typeof asset.contentType !== "string")) {
158
+ throw new Error(`Storage asset manifest entry ${index + 1} has an invalid shape.`);
159
+ }
160
+ const destination = `${asset.bucket}/${asset.objectPath}`;
161
+ if (destinations.has(destination)) {
162
+ throw new Error(`Duplicate Storage asset destination: ${destination}.`);
163
+ }
164
+ destinations.add(destination);
165
+ const assetFile = resolveProjectInput(projectRoot, asset.file, `Storage asset file in entry ${index + 1}`);
166
+ await assertRegularInput(assetFile, `Storage asset file in entry ${index + 1}`);
167
+ }
168
+ return { assetCount: manifest.length };
169
+ };
170
+ export const inspectBaselineInputFiles = async ({ projectRoot = process.cwd(), configPath, recordsPath, ledgerPath, assetsPath, }) => {
171
+ const loaded = await loadRehearsalConfig({
172
+ projectRoot,
173
+ ...(configPath === undefined ? {} : { configPath }),
174
+ });
175
+ const records = resolveProjectInput(loaded.projectRoot, recordsPath, "Synthetic records path");
176
+ const ledger = resolveProjectInput(loaded.projectRoot, ledgerPath, "Migration ledger path");
177
+ const assets = assetsPath
178
+ ? resolveProjectInput(loaded.projectRoot, assetsPath, "Storage asset manifest path")
179
+ : undefined;
180
+ await Promise.all([
181
+ assertRegularInput(records, "Synthetic records path"),
182
+ assertRegularInput(ledger, "Migration ledger path"),
183
+ ...(assets
184
+ ? [assertRegularInput(assets, "Storage asset manifest path")]
185
+ : []),
186
+ ]);
187
+ let ledgerRows;
188
+ try {
189
+ ledgerRows = JSON.parse(await readFile(ledger, "utf8"));
190
+ }
191
+ catch (error) {
192
+ throw new Error("Migration ledger contains invalid JSON.", {
193
+ cause: error,
194
+ });
195
+ }
196
+ const [inspection, assetInspection] = await Promise.all([
197
+ inspectSyntheticRecords(records),
198
+ assets
199
+ ? inspectAssetManifest(assets, loaded.projectRoot)
200
+ : Promise.resolve({ assetCount: 0 }),
201
+ ]);
202
+ const migrationHistory = buildMigrationLedgerInventory(ledgerRows, "Synthetic migration ledger");
203
+ const latestMigration = migrationHistory.at(-1);
204
+ if (!latestMigration) {
205
+ throw new Error("Synthetic migration ledger is empty or unavailable.");
206
+ }
207
+ return {
208
+ projectRoot: loaded.projectRoot,
209
+ recordsPath: relative(loaded.projectRoot, records),
210
+ ledgerPath: relative(loaded.projectRoot, ledger),
211
+ ...(assets ? { assetsPath: relative(loaded.projectRoot, assets) } : {}),
212
+ migrationHistory,
213
+ migrationCutoff: latestMigration.version,
214
+ migrationCount: migrationHistory.length,
215
+ rowCount: inspection.rowCount,
216
+ tables: inspection.tables,
217
+ assetCount: assetInspection.assetCount,
218
+ };
219
+ };
220
+ const renderPolicyDraft = ({ migrationCutoff, tables, }) => `${JSON.stringify({
221
+ policyVersion: 1,
222
+ draft: true,
223
+ migrationCutoff,
224
+ tables: tables.map((table) => ({
225
+ ...(table.schema === "public" ? {} : { schema: table.schema }),
226
+ name: table.name,
227
+ group: "synthetic",
228
+ sourceRows: "STREAM AND SANITIZE",
229
+ columns: table.columns.map((name) => ({
230
+ name,
231
+ action: "REVIEW REQUIRED",
232
+ generated: "REVIEW REQUIRED",
233
+ identity: "REVIEW REQUIRED",
234
+ foreignKey: "REVIEW REQUIRED",
235
+ })),
236
+ })),
237
+ }, null, 2)}\n`;
238
+ export const planBaselinePreparation = async ({ projectRoot = process.cwd(), configPath, recordsPath, ledgerPath, }) => {
239
+ const loaded = await loadRehearsalConfig({
240
+ projectRoot,
241
+ ...(configPath === undefined ? {} : { configPath }),
242
+ });
243
+ const inspection = await inspectBaselineInputFiles({
244
+ projectRoot: loaded.projectRoot,
245
+ ...(configPath === undefined ? {} : { configPath }),
246
+ recordsPath,
247
+ ledgerPath,
248
+ });
249
+ const destination = loaded.paths.sanitizationPolicy;
250
+ if (await pathExists(destination)) {
251
+ throw new Error(`Rehearsal baseline preparation will not overwrite ${relative(loaded.projectRoot, destination)}.`);
252
+ }
253
+ return {
254
+ projectRoot: loaded.projectRoot,
255
+ destination,
256
+ destinationRelative: relative(loaded.projectRoot, destination),
257
+ recordsPath: inspection.recordsPath,
258
+ ledgerPath: inspection.ledgerPath,
259
+ migrationCutoff: inspection.migrationCutoff,
260
+ migrationCount: inspection.migrationCount,
261
+ rowCount: inspection.rowCount,
262
+ tables: inspection.tables,
263
+ content: renderPolicyDraft({
264
+ migrationCutoff: inspection.migrationCutoff,
265
+ tables: inspection.tables,
266
+ }),
267
+ };
268
+ };
269
+ export const applyBaselinePreparation = async (plan) => {
270
+ if (await pathExists(plan.destination)) {
271
+ throw new Error(`Rehearsal baseline preparation will not overwrite ${plan.destinationRelative}.`);
272
+ }
273
+ await mkdir(dirname(plan.destination), { recursive: true, mode: 0o700 });
274
+ const handle = await open(plan.destination, "wx", 0o600);
275
+ try {
276
+ await handle.writeFile(plan.content);
277
+ await handle.sync();
278
+ }
279
+ finally {
280
+ await handle.close();
281
+ }
282
+ };
283
+ export const summarizeBaselinePreparation = (plan, { mode }) => ({
284
+ mode,
285
+ destination: plan.destinationRelative,
286
+ recordsPath: plan.recordsPath,
287
+ ledgerPath: plan.ledgerPath,
288
+ migrationCutoff: plan.migrationCutoff,
289
+ migrationCount: plan.migrationCount,
290
+ rowCount: plan.rowCount,
291
+ tables: plan.tables,
292
+ nextAction: mode === "written"
293
+ ? `Review every REVIEW REQUIRED decision in ${plan.destinationRelative}, remove draft only after review, then create the baseline.`
294
+ : "Review this schema-only summary, then rerun with --write to create the draft.",
295
+ });
@@ -0,0 +1,147 @@
1
+ /** Typed, normalized contract shared by privacy validation and execution stages. */
2
+ export type PrivacyAction = "KEEP" | "PSEUDONYMIZE" | "REPLACE" | "EXCLUDE" | "DERIVE";
3
+ export type PrivacyObject = Record<string, unknown>;
4
+ export interface PseudonymRecipe {
5
+ format: "uuid" | "email" | "text" | "integer" | "hex" | "gtin" | "url";
6
+ namespace: string;
7
+ maxLength?: number;
8
+ length?: number;
9
+ allowedLengths?: readonly number[];
10
+ origin?: string;
11
+ }
12
+ export interface ConstantRecipe {
13
+ kind: "constant";
14
+ value: unknown;
15
+ }
16
+ export interface DateShiftRecipe {
17
+ kind: "date-shift";
18
+ days: number;
19
+ representation: "iso-string" | "epoch-milliseconds";
20
+ group?: string;
21
+ }
22
+ export interface EnumRecipe {
23
+ kind: "enum";
24
+ allowNull: boolean;
25
+ values: readonly (string | number | boolean)[];
26
+ }
27
+ export interface ValidatedStringRecipe {
28
+ kind: "validated-string";
29
+ format: "portable-code";
30
+ maximumBytes: number;
31
+ allowNull: boolean;
32
+ }
33
+ export interface DigestRecipe {
34
+ kind: "digest";
35
+ format: "hex";
36
+ length: number;
37
+ namespace: string;
38
+ inputs: readonly string[];
39
+ }
40
+ export interface BindingSubstituteRecipe {
41
+ kind: "binding-substitute";
42
+ binding: string;
43
+ format: "uuid";
44
+ namespace: string;
45
+ maximumBytes: number;
46
+ }
47
+ export interface PathMapRecipe {
48
+ kind: "path-map";
49
+ mapping: string;
50
+ }
51
+ export interface ApprovedOwnerRecipe {
52
+ kind: "approved-owner";
53
+ approved: NormalizedDeclaration;
54
+ otherwise: NormalizedDeclaration;
55
+ }
56
+ export interface JsonArrayRecipe {
57
+ kind: "json-array";
58
+ allowNull: boolean;
59
+ maximumBytes: number;
60
+ maximumDepth: number;
61
+ maximumItems: number;
62
+ items: NormalizedDeclaration;
63
+ }
64
+ export interface JsonObjectRecipe {
65
+ kind: "json-object";
66
+ allowNull: boolean;
67
+ maximumBytes: number;
68
+ maximumDepth: number;
69
+ fields: Readonly<Record<string, NormalizedDeclaration>>;
70
+ }
71
+ export interface JsonUnionRecipe {
72
+ kind: "json-union";
73
+ allowNull: boolean;
74
+ maximumBytes: number;
75
+ maximumDepth: number;
76
+ maximumItems: number;
77
+ variants: Readonly<Record<string, NormalizedDeclaration>>;
78
+ }
79
+ export interface JsonDictionaryRecipe {
80
+ kind: "json-dictionary";
81
+ allowNull: boolean;
82
+ maximumBytes: number;
83
+ maximumDepth: number;
84
+ maximumItems: number;
85
+ keys: Readonly<Record<string, unknown>>;
86
+ values: NormalizedDeclaration;
87
+ }
88
+ export type DerivationRecipe = DateShiftRecipe | EnumRecipe | ValidatedStringRecipe | DigestRecipe | BindingSubstituteRecipe | PathMapRecipe | ApprovedOwnerRecipe | JsonArrayRecipe | JsonObjectRecipe | JsonUnionRecipe | JsonDictionaryRecipe;
89
+ export type NormalizedRecipe = PseudonymRecipe | ConstantRecipe | DerivationRecipe | null;
90
+ export interface NormalizedDeclaration {
91
+ action: PrivacyAction;
92
+ recipe: NormalizedRecipe;
93
+ required?: boolean;
94
+ }
95
+ export interface PrivacyBinding {
96
+ environmentVariable: string;
97
+ approvedValueSha256: string;
98
+ }
99
+ export interface PrivacyPathMapping extends PseudonymRecipe {
100
+ binding: string;
101
+ format: "uuid" | "text";
102
+ maximumBytes: number;
103
+ maximumSegments: number;
104
+ }
105
+ export interface PrivacyForeignKey {
106
+ readonly schema: string;
107
+ readonly table: string;
108
+ readonly column: string;
109
+ }
110
+ export interface PrivacyColumn extends NormalizedDeclaration {
111
+ name: string;
112
+ generated: "ALWAYS" | "NEVER";
113
+ identity: "YES" | "NO";
114
+ foreignKey: PrivacyForeignKey | null;
115
+ }
116
+ export interface PrivacyTable {
117
+ schema: string;
118
+ name: string;
119
+ sourceRows: "STREAM AND SANITIZE" | "EXCLUDE";
120
+ ownerBinding: Readonly<{
121
+ binding: string;
122
+ columns: readonly string[];
123
+ match: "any";
124
+ nullBehavior?: "otherwise";
125
+ }> | null;
126
+ columns: readonly PrivacyColumn[];
127
+ }
128
+ export interface ExecutablePrivacyPolicy {
129
+ readonly policyVersion: 2;
130
+ readonly migrationCutoff: string;
131
+ readonly bindings: Readonly<Record<string, PrivacyBinding>>;
132
+ readonly pathMappings: Readonly<Record<string, PrivacyPathMapping>>;
133
+ readonly tables: readonly PrivacyTable[];
134
+ }
135
+ export interface PrivacySourceRecord {
136
+ schema?: string;
137
+ table: string;
138
+ row: Record<string, unknown>;
139
+ }
140
+ export interface PrivacyEngine {
141
+ readonly keyFingerprint: string;
142
+ remapPath(input: {
143
+ mapping: string;
144
+ value: string;
145
+ }): string;
146
+ sanitize(record: unknown): PrivacySourceRecord | null;
147
+ }
@@ -0,0 +1,2 @@
1
+ /** Typed, normalized contract shared by privacy validation and execution stages. */
2
+ export {};
@@ -0,0 +1,7 @@
1
+ /** Validate cross-rule bindings, dependencies, and shared deterministic groups. */
2
+ import type { PrivacyBinding, PrivacyPathMapping, PrivacyTable } from "./privacy_contract.mjs";
3
+ export declare const validatePrivacyDependencyGraph: ({ tables, bindings, pathMappings, }: {
4
+ tables: readonly PrivacyTable[];
5
+ bindings: Readonly<Record<string, PrivacyBinding>>;
6
+ pathMappings: Readonly<Record<string, PrivacyPathMapping>>;
7
+ }) => void;
@@ -0,0 +1,110 @@
1
+ /** Validate cross-rule bindings, dependencies, and shared deterministic groups. */
2
+ import { structuredJsonDeclarations } from "./privacy_structured_json.mjs";
3
+ const usesApprovedOwner = (declaration) => (declaration.recipe !== null &&
4
+ "kind" in declaration.recipe &&
5
+ declaration.recipe.kind === "approved-owner") ||
6
+ structuredJsonDeclarations(declaration.recipe).some(usesApprovedOwner);
7
+ const visitRecipes = (declaration, visit) => {
8
+ visit(declaration.recipe);
9
+ if (declaration.recipe !== null &&
10
+ "kind" in declaration.recipe &&
11
+ declaration.recipe.kind === "approved-owner") {
12
+ visitRecipes(declaration.recipe.approved, visit);
13
+ visitRecipes(declaration.recipe.otherwise, visit);
14
+ }
15
+ else {
16
+ structuredJsonDeclarations(declaration.recipe).forEach((nested) => visitRecipes(nested, visit));
17
+ }
18
+ };
19
+ const digestInputs = (declaration) => {
20
+ if (declaration.recipe !== null &&
21
+ "kind" in declaration.recipe &&
22
+ declaration.recipe.kind === "digest") {
23
+ return [...declaration.recipe.inputs];
24
+ }
25
+ if (declaration.recipe !== null &&
26
+ "kind" in declaration.recipe &&
27
+ declaration.recipe.kind === "approved-owner") {
28
+ return [
29
+ ...digestInputs(declaration.recipe.approved),
30
+ ...digestInputs(declaration.recipe.otherwise),
31
+ ];
32
+ }
33
+ return structuredJsonDeclarations(declaration.recipe).flatMap(digestInputs);
34
+ };
35
+ const validateTableDependencies = ({ table, index, bindings, pathMappings, }) => {
36
+ const label = `policy.tables[${index}]`;
37
+ if (table.columns.some(usesApprovedOwner) && !table.ownerBinding) {
38
+ throw new Error(`${label} uses approved-owner without ownerBinding.`);
39
+ }
40
+ for (const column of table.columns) {
41
+ visitRecipes(column, (recipe) => {
42
+ if (recipe &&
43
+ "kind" in recipe &&
44
+ recipe.kind === "path-map" &&
45
+ !Object.hasOwn(pathMappings, recipe.mapping)) {
46
+ throw new Error(`${label} references unknown path mapping ${recipe.mapping}.`);
47
+ }
48
+ if (recipe !== null &&
49
+ "kind" in recipe &&
50
+ recipe.kind === "binding-substitute" &&
51
+ !Object.hasOwn(bindings, recipe.binding)) {
52
+ throw new Error(`${label} references unknown privacy binding ${recipe.binding}.`);
53
+ }
54
+ });
55
+ }
56
+ const dependencies = new Map(table.columns.map((column) => [column.name, []]));
57
+ for (const column of table.columns) {
58
+ const inputs = [...new Set(digestInputs(column))];
59
+ for (const input of inputs) {
60
+ const inputColumn = table.columns.find((entry) => entry.name === input);
61
+ if (!inputColumn) {
62
+ throw new Error(`${label}.${column.name} digest references unknown column ${input}.`);
63
+ }
64
+ if (inputColumn.action === "EXCLUDE") {
65
+ throw new Error(`${label}.${column.name} digest input ${input} is excluded instead of sanitized.`);
66
+ }
67
+ }
68
+ dependencies.set(column.name, inputs);
69
+ }
70
+ const resolved = new Set();
71
+ const resolving = new Set();
72
+ const visitDependency = (columnName) => {
73
+ if (resolved.has(columnName))
74
+ return;
75
+ if (resolving.has(columnName)) {
76
+ throw new Error(`${label} contains a cyclic digest dependency.`);
77
+ }
78
+ resolving.add(columnName);
79
+ for (const input of dependencies.get(columnName) ?? []) {
80
+ visitDependency(input);
81
+ }
82
+ resolving.delete(columnName);
83
+ resolved.add(columnName);
84
+ };
85
+ table.columns.forEach((column) => visitDependency(column.name));
86
+ };
87
+ export const validatePrivacyDependencyGraph = ({ tables, bindings, pathMappings, }) => {
88
+ tables.forEach((table, index) => validateTableDependencies({ table, index, bindings, pathMappings }));
89
+ const dateShiftGroups = new Map();
90
+ const validateDateShiftGroup = (declaration) => {
91
+ const recipe = declaration.recipe;
92
+ if (!recipe || !("kind" in recipe))
93
+ return;
94
+ if (recipe.kind === "date-shift" && recipe.group) {
95
+ const existing = dateShiftGroups.get(recipe.group);
96
+ if (existing !== undefined && existing !== recipe.days) {
97
+ throw new Error(`Privacy date-shift group ${recipe.group} must use one days value.`);
98
+ }
99
+ dateShiftGroups.set(recipe.group, recipe.days);
100
+ }
101
+ if (recipe.kind === "approved-owner") {
102
+ validateDateShiftGroup(recipe.approved);
103
+ validateDateShiftGroup(recipe.otherwise);
104
+ }
105
+ else {
106
+ structuredJsonDeclarations(recipe).forEach(validateDateShiftGroup);
107
+ }
108
+ };
109
+ tables.forEach((table) => table.columns.forEach(validateDateShiftGroup));
110
+ };
@@ -0,0 +1,5 @@
1
+ /** Stable public facade for the versioned, fail-closed privacy pipeline. */
2
+ export { validateExecutablePrivacyPolicy } from "./privacy_policy.mjs";
3
+ export { createPrivacyEngine } from "./privacy_pipeline.mjs";
4
+ export { assertPrivacyKeyFingerprint, createPrivacyKey, readPrivacyKey, } from "./privacy_key.mjs";
5
+ export type { ExecutablePrivacyPolicy, PrivacyColumn, PrivacyEngine, PrivacyForeignKey, PrivacySourceRecord, PrivacyTable, } from "./privacy_policy.mjs";
@@ -0,0 +1,4 @@
1
+ /** Stable public facade for the versioned, fail-closed privacy pipeline. */
2
+ export { validateExecutablePrivacyPolicy } from "./privacy_policy.mjs";
3
+ export { createPrivacyEngine } from "./privacy_pipeline.mjs";
4
+ export { assertPrivacyKeyFingerprint, createPrivacyKey, readPrivacyKey, } from "./privacy_key.mjs";
@@ -0,0 +1,7 @@
1
+ /** Create and verify the owner-only secret used by deterministic privacy rules. */
2
+ export declare const PRIVACY_KEY_BYTES = 32;
3
+ export declare const createPrivacyKey: (path: string) => Promise<{
4
+ fingerprint: string;
5
+ }>;
6
+ export declare const readPrivacyKey: (path: string) => Promise<Buffer>;
7
+ export declare const assertPrivacyKeyFingerprint: (key: Uint8Array, expected: unknown) => void;
@@ -0,0 +1,42 @@
1
+ /** Create and verify the owner-only secret used by deterministic privacy rules. */
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { chmod, mkdir, open, readFile, stat } from "node:fs/promises";
4
+ import { dirname } from "node:path";
5
+ export const PRIVACY_KEY_BYTES = 32;
6
+ const HEX_64 = /^[a-f0-9]{64}$/u;
7
+ export const createPrivacyKey = async (path) => {
8
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
9
+ const key = randomBytes(PRIVACY_KEY_BYTES);
10
+ const handle = await open(path, "wx", 0o600);
11
+ try {
12
+ await handle.writeFile(`${key.toString("base64")}\n`);
13
+ await handle.sync();
14
+ }
15
+ finally {
16
+ await handle.close();
17
+ }
18
+ await chmod(path, 0o600);
19
+ return { fingerprint: createHash("sha256").update(key).digest("hex") };
20
+ };
21
+ export const readPrivacyKey = async (path) => {
22
+ const details = await stat(path);
23
+ if (!details.isFile() || (details.mode & 0o077) !== 0) {
24
+ throw new Error("Privacy key must be an owner-only regular file.");
25
+ }
26
+ const encoded = (await readFile(path, "utf8")).trim();
27
+ const key = Buffer.from(encoded, "base64");
28
+ if (key.length !== PRIVACY_KEY_BYTES ||
29
+ !/^[A-Za-z0-9+/]+={0,2}$/u.test(encoded)) {
30
+ throw new Error("Privacy key file is invalid.");
31
+ }
32
+ return key;
33
+ };
34
+ export const assertPrivacyKeyFingerprint = (key, expected) => {
35
+ if (typeof expected !== "string" || !HEX_64.test(expected)) {
36
+ throw new Error("Privacy key fingerprint is invalid.");
37
+ }
38
+ const actual = createHash("sha256").update(key).digest("hex");
39
+ if (actual !== expected) {
40
+ throw new Error("Privacy key does not match the reviewed baseline receipt.");
41
+ }
42
+ };
@@ -0,0 +1,7 @@
1
+ /** Resolve reviewed bindings and execute the dependency-aware row privacy pipeline. */
2
+ import type { PrivacyEngine } from "./privacy_contract.mjs";
3
+ export declare const createPrivacyEngine: ({ policy, key, environment, }: {
4
+ policy: unknown;
5
+ key: Uint8Array | string;
6
+ environment?: NodeJS.ProcessEnv;
7
+ }) => Readonly<PrivacyEngine>;