@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
@@ -39,10 +39,578 @@ Two policy versions exist:
39
39
  - Version 2 is required by `baseline refresh`. It adds bounded declarative recipes so
40
40
  Rehearsal can sanitize without project callbacks.
41
41
 
42
- Version 2 supports keyed UUID, email, text, and integer pseudonyms; explicit constant
43
- replacement; bounded date shifting; and exhaustively classified JSON objects. Unknown
44
- tables, columns, nested keys, formats, recipes, or missing fields fail closed. It never
45
- evaluates JavaScript or SQL from the policy.
42
+ Version 2 supports keyed UUID, email, text, integer, fixed-width hexadecimal, and GTIN
43
+ pseudonyms; digests derived from named sanitized columns; explicit constant replacement;
44
+ bounded and grouped date shifting; identity-aware path mapping;
45
+ approved-owner conditionals; exhaustively classified JSON objects; bounded JSON arrays;
46
+ reviewed JSON type unions; and runtime-keyed JSON dictionaries. Unknown tables, columns,
47
+ nested keys, formats, recipes, variants, or missing required fields fail closed. It never
48
+ evaluates JavaScript, regular expressions, or SQL from the policy.
49
+
50
+ ## Approved-owner values
51
+
52
+ Keep the raw approved owner outside the policy and baseline. The policy stores only its
53
+ environment-variable name and SHA-256 receipt:
54
+
55
+ ```json
56
+ {
57
+ "bindings": {
58
+ "approved-owner": {
59
+ "environmentVariable": "REHEARSAL_APPROVED_OWNER_ID",
60
+ "approvedValueSha256": "<lowercase sha256 of the exact owner value>"
61
+ }
62
+ },
63
+ "tables": [
64
+ {
65
+ "name": "profiles",
66
+ "sourceRows": "STREAM AND SANITIZE",
67
+ "ownerBinding": { "binding": "approved-owner", "column": "user_id" },
68
+ "columns": [
69
+ {
70
+ "name": "user_id",
71
+ "action": "PSEUDONYMIZE",
72
+ "recipe": { "format": "uuid", "namespace": "account-id" },
73
+ "generated": "NEVER",
74
+ "identity": "NO",
75
+ "foreignKey": null
76
+ },
77
+ {
78
+ "name": "display_name",
79
+ "action": "DERIVE",
80
+ "recipe": {
81
+ "kind": "approved-owner",
82
+ "approved": { "action": "KEEP" },
83
+ "otherwise": {
84
+ "action": "REPLACE",
85
+ "recipe": { "kind": "constant", "value": "Synthetic account" }
86
+ }
87
+ },
88
+ "generated": "NEVER",
89
+ "identity": "NO",
90
+ "foreignKey": null
91
+ }
92
+ ]
93
+ }
94
+ ]
95
+ }
96
+ ```
97
+
98
+ Generate the receipt without putting the owner value in shell history:
99
+
100
+ ```sh
101
+ node -e 'const {createHash}=require("node:crypto"); process.stdout.write(createHash("sha256").update(process.env.REHEARSAL_APPROVED_OWNER_ID).digest("hex")+"\n")'
102
+ ```
103
+
104
+ Every conditional table names its owner column. Missing bindings, hash mismatches,
105
+ missing owner columns, unsupported owner values, and conditionals without an owner
106
+ binding are refused. The owner column must use `PSEUDONYMIZE`, preventing a conditional
107
+ policy from retaining raw account IDs.
108
+
109
+ When ownership may come through several relationships, replace `column` with an explicit
110
+ bounded any-of declaration:
111
+
112
+ ```json
113
+ {
114
+ "ownerBinding": {
115
+ "binding": "approved-owner",
116
+ "columns": ["target_user_id", "actor_user_id"],
117
+ "match": "any",
118
+ "nullBehavior": "otherwise"
119
+ }
120
+ }
121
+ ```
122
+
123
+ Every listed column must exist and be pseudonymized. One matching value selects the
124
+ `approved` branch. `nullBehavior: "otherwise"` explicitly treats a null value as a
125
+ non-match; it never grants owner access. Without that declaration, encountering null is
126
+ an error. Empty strings, non-finite numbers (`NaN`, positive infinity, or negative
127
+ infinity), objects, arrays, booleans, missing columns, duplicate declarations, and
128
+ unsupported matching modes remain errors. Rehearsal validates every declared owner
129
+ column before selecting either branch, even when another owner column matches. A
130
+ single-column binding may also declare `nullBehavior: "otherwise"` when that column is
131
+ reviewed as nullable.
132
+
133
+ `identity` describes PostgreSQL's generated SQL identity metadata, not whether a value
134
+ identifies a person: ordinary UUID primary keys use `"NO"`, while a genuine
135
+ `GENERATED ... AS IDENTITY` column uses `"YES"`. Rehearsal checks the restored PostgreSQL
136
+ schema for a real backing sequence before attempting a sequence reset, so UUID identity
137
+ values never receive numeric sequence operations. Other owners take the declared
138
+ `otherwise` path; every other identity-bearing field still needs its own pseudonymization
139
+ declaration. Supply the binding only to the local server-side Rehearsal process; do not
140
+ put it in the policy, baseline, or tracked files.
141
+
142
+ ## Identity-aware Storage paths
143
+
144
+ Use one named mapping when a database column and a physical Storage object both begin
145
+ with the approved owner's ID. Rehearsal pseudonymizes only that first segment and keeps
146
+ the remaining path unchanged:
147
+
148
+ ```json
149
+ {
150
+ "pathMappings": {
151
+ "owner-storage": {
152
+ "binding": "approved-owner",
153
+ "format": "uuid",
154
+ "namespace": "account-id",
155
+ "maximumBytes": 512,
156
+ "maximumSegments": 8
157
+ }
158
+ },
159
+ "tables": [
160
+ {
161
+ "name": "profiles",
162
+ "sourceRows": "STREAM AND SANITIZE",
163
+ "columns": [
164
+ {
165
+ "name": "avatar_path",
166
+ "action": "DERIVE",
167
+ "recipe": { "kind": "path-map", "mapping": "owner-storage" },
168
+ "generated": "NEVER",
169
+ "identity": "NO",
170
+ "foreignKey": null
171
+ }
172
+ ]
173
+ }
174
+ ]
175
+ }
176
+ ```
177
+
178
+ The source policy uses the same mapping name and names the binding's environment
179
+ variable as `prefixEnvironmentVariable`; it never contains the raw owner ID. During
180
+ `rehearsal refresh`, the package uses the mapping for both database values and physical
181
+ object destinations. A missing mapping, a different environment variable, a path whose
182
+ first segment is not the exact reviewed owner, traversal, or a size/segment limit breach
183
+ stops the refresh.
184
+
185
+ ## Related dates
186
+
187
+ Give related timestamps the same `group` and `days` value to shift them by one stable
188
+ offset. This preserves their order and duration:
189
+
190
+ ```json
191
+ {
192
+ "name": "starts_at",
193
+ "action": "DERIVE",
194
+ "recipe": { "kind": "date-shift", "days": 30, "group": "event-window" },
195
+ "generated": "NEVER",
196
+ "identity": "NO",
197
+ "foreignKey": null
198
+ }
199
+ ```
200
+
201
+ Apply the same recipe to `ends_at`. Rehearsal refuses a group that declares conflicting
202
+ `days` values. Leave out `group` when a date should shift independently.
203
+
204
+ `date-shift` returns an ISO string by default, preserving the existing behavior. When
205
+ the reviewed application model stores a date as numeric epoch milliseconds, declare
206
+ that representation explicitly:
207
+
208
+ ```json
209
+ {
210
+ "action": "DERIVE",
211
+ "recipe": {
212
+ "kind": "date-shift",
213
+ "days": 30,
214
+ "group": "saved-recipe-window",
215
+ "representation": "epoch-milliseconds"
216
+ }
217
+ }
218
+ ```
219
+
220
+ This form accepts only finite safe integers that JavaScript can represent as dates and
221
+ returns a numeric epoch-millisecond value. It preserves `null`, refuses strings,
222
+ fractions, non-finite numbers, unsafe integers and values outside the supported date
223
+ range. ISO-string and epoch-millisecond declarations may share a group; they receive the
224
+ same stable offset while each retains its declared output representation. The only
225
+ supported representation names are `iso-string` and `epoch-milliseconds`.
226
+
227
+ ## Constraint-preserving identifiers and fingerprints
228
+
229
+ Use a keyed `hex` pseudonym when one source value must remain a lowercase 32- or
230
+ 64-character hexadecimal value:
231
+
232
+ ```json
233
+ {
234
+ "name": "source_fingerprint",
235
+ "action": "PSEUDONYMIZE",
236
+ "recipe": {
237
+ "format": "hex",
238
+ "namespace": "source-fingerprint",
239
+ "length": 64
240
+ },
241
+ "generated": "NEVER",
242
+ "identity": "NO",
243
+ "foreignKey": null
244
+ }
245
+ ```
246
+
247
+ Use a `digest` derivation when the fingerprint must describe other sanitized fields.
248
+ Inputs are column names, evaluated in the listed order after their own privacy recipes.
249
+ The existing raw fingerprint is ignored:
250
+
251
+ ```json
252
+ {
253
+ "name": "evidence_fingerprint",
254
+ "action": "DERIVE",
255
+ "recipe": {
256
+ "kind": "digest",
257
+ "format": "hex",
258
+ "namespace": "evidence-fingerprint",
259
+ "length": 32,
260
+ "inputs": ["sanitized_owner_id", "evidence_state"]
261
+ },
262
+ "generated": "NEVER",
263
+ "identity": "NO",
264
+ "foreignKey": null
265
+ }
266
+ ```
267
+
268
+ Digest inputs must exist, must not be excluded, and cannot form a dependency cycle.
269
+ Rehearsal supports only 32- and 64-character hexadecimal output. The namespace and local
270
+ privacy key separate otherwise identical declarations.
271
+
272
+ Use the `gtin` pseudonym format for a normalized GTIN whose length and check digit must
273
+ remain valid:
274
+
275
+ ```json
276
+ {
277
+ "name": "barcode",
278
+ "action": "PSEUDONYMIZE",
279
+ "recipe": { "format": "gtin", "namespace": "product-barcode", "length": 14 },
280
+ "generated": "NEVER",
281
+ "identity": "NO",
282
+ "foreignKey": null
283
+ }
284
+ ```
285
+
286
+ Supported GTIN lengths are 8, 12, 13, and 14. Non-null input must be a normalized string
287
+ of exactly that length with a valid check digit; malformed input stops preparation. Null
288
+ stays null. Rehearsal creates a deterministic keyed payload and recalculates the check
289
+ digit, so related fields using the same namespace, length, source value, and key remain
290
+ aligned.
291
+
292
+ When one reviewed field legitimately contains more than one GTIN length, replace
293
+ `"length": 14` with an explicit allowlist such as `"allowedLengths": [12, 14]`. The two
294
+ keys are mutually exclusive. Each source value must match one allowed length, and its
295
+ result retains that exact length. Empty, duplicated, or unsupported allowlist entries
296
+ are refused. This also works inside structured JSON declarations; a JSON union is not
297
+ needed merely to distinguish GTIN string lengths.
298
+
299
+ Use the `url` pseudonym format when a sanitized field must remain an HTTPS URL:
300
+
301
+ ```json
302
+ {
303
+ "name": "evidence_reference",
304
+ "action": "PSEUDONYMIZE",
305
+ "recipe": {
306
+ "format": "url",
307
+ "namespace": "evidence-url",
308
+ "origin": "https://127.0.0.1:58432",
309
+ "maxLength": 500
310
+ },
311
+ "generated": "NEVER",
312
+ "identity": "NO",
313
+ "foreignKey": null
314
+ }
315
+ ```
316
+
317
+ `origin` is required and must be an HTTPS loopback origin using `127.0.0.1`, `::1`, or
318
+ `localhost`. Credentials, paths, queries, fragments, non-HTTPS schemes, and non-local
319
+ hosts are refused. The value must be the canonical origin itself, optionally followed by
320
+ one `/`; dot segments and encoded dot segments are rejected before URL normalization.
321
+ `maxLength` is also required, cannot exceed 2,048 characters, and must fit the complete
322
+ generated URL.
323
+
324
+ Non-null input must be a valid HTTPS URL without surrounding or embedded whitespace.
325
+ Rehearsal hashes the entire source value with the local privacy key and namespace, then
326
+ emits only the reviewed origin and a 32-character pseudonymous path. It never copies the
327
+ source host, credentials, path, query, token, or fragment. Null remains null. Equal
328
+ inputs using the same namespace, key, and origin correspond across ordinary columns and
329
+ structured JSON; different inputs and rotated keys produce different paths.
330
+
331
+ The same declaration can be used for items inside bounded `json-array` recipes, which
332
+ covers PostgreSQL text-array projections represented by the source pipeline. Each array
333
+ and object still needs its ordinary item, depth, and byte bounds.
334
+
335
+ ### Retained text containing a reviewed identity
336
+
337
+ Use `binding-substitute` when permitted text may contain the exact value of a reviewed
338
+ UUID binding. This keeps the surrounding text but replaces every occurrence of that UUID
339
+ with the same keyed pseudonym used by a `uuid` declaration sharing its namespace:
340
+
341
+ ```json
342
+ {
343
+ "action": "DERIVE",
344
+ "recipe": {
345
+ "kind": "approved-owner",
346
+ "approved": {
347
+ "action": "DERIVE",
348
+ "recipe": {
349
+ "kind": "binding-substitute",
350
+ "binding": "approved-owner",
351
+ "format": "uuid",
352
+ "namespace": "account-id",
353
+ "maximumBytes": 256
354
+ }
355
+ },
356
+ "otherwise": {
357
+ "action": "PSEUDONYMIZE",
358
+ "recipe": {
359
+ "format": "text",
360
+ "namespace": "food-description",
361
+ "maxLength": 128
362
+ }
363
+ }
364
+ }
365
+ }
366
+ ```
367
+
368
+ The named binding must already exist in `policy.bindings`, and its environment value must
369
+ match the tracked SHA-256 review receipt. For this recipe the reviewed value must be a
370
+ UUID. `maximumBytes` is required and applies before and after replacement. Null remains
371
+ null; other input must be text. Ordinary text is unchanged, literal UUID occurrences are
372
+ all replaced, and the raw identity is never stored in policy.
373
+
374
+ This is intentionally not a general search-and-replace facility: there are no regular
375
+ expressions, arbitrary patterns, replacement strings, or callbacks. Pair it with an
376
+ `approved-owner` branch when only the reviewed owner's surrounding text may be retained.
377
+
378
+ ## Structured JSON
379
+
380
+ `json-object` fields are required by default. Set `"required": false` on an explicitly
381
+ optional field. Unknown keys remain errors. `json-array` declares one item recipe and a
382
+ required `maximumItems`. Both recipes accept `maximumDepth`, `maximumBytes`, and
383
+ `allowNull`; defaults are 8 levels, 65,536 bytes, and no null container. Bounds are
384
+ checked before producing the transformed structure. Nested objects and arrays repeat
385
+ the same declarations, and every nested identity uses an explicit pseudonym recipe.
386
+
387
+ ```json
388
+ {
389
+ "action": "DERIVE",
390
+ "recipe": {
391
+ "kind": "json-array",
392
+ "maximumItems": 25,
393
+ "maximumDepth": 4,
394
+ "maximumBytes": 8192,
395
+ "items": {
396
+ "action": "DERIVE",
397
+ "recipe": {
398
+ "kind": "json-object",
399
+ "fields": {
400
+ "actor_id": {
401
+ "action": "PSEUDONYMIZE",
402
+ "recipe": { "format": "uuid", "namespace": "account-id" }
403
+ },
404
+ "note": {
405
+ "action": "REPLACE",
406
+ "required": false,
407
+ "recipe": { "kind": "constant", "value": "Synthetic note" }
408
+ }
409
+ }
410
+ }
411
+ }
412
+ }
413
+ }
414
+ ```
415
+
416
+ ### JSON values with several reviewed types
417
+
418
+ Use `json-union` when one JSON column legitimately stores more than one root type. Every
419
+ accepted type has its own declaration. Null is rejected unless `allowNull` is explicitly
420
+ true. Arrays and objects cannot use whole-value `KEEP`; their contents must be classified
421
+ through `json-array`, `json-object`, `json-dictionary`, another bounded union, or a safe
422
+ replacement or pseudonymization.
423
+
424
+ ```json
425
+ {
426
+ "action": "DERIVE",
427
+ "recipe": {
428
+ "kind": "json-union",
429
+ "allowNull": true,
430
+ "maximumItems": 25,
431
+ "maximumDepth": 5,
432
+ "maximumBytes": 8192,
433
+ "variants": {
434
+ "string": { "action": "KEEP" },
435
+ "number": { "action": "KEEP" },
436
+ "array": {
437
+ "action": "DERIVE",
438
+ "recipe": {
439
+ "kind": "json-array",
440
+ "maximumItems": 25,
441
+ "items": {
442
+ "action": "REPLACE",
443
+ "recipe": { "kind": "constant", "value": "Synthetic item" }
444
+ }
445
+ }
446
+ },
447
+ "object": {
448
+ "action": "DERIVE",
449
+ "recipe": {
450
+ "kind": "json-object",
451
+ "fields": {
452
+ "status": { "action": "KEEP" },
453
+ "note": {
454
+ "action": "REPLACE",
455
+ "required": false,
456
+ "recipe": { "kind": "constant", "value": "Synthetic note" }
457
+ }
458
+ }
459
+ }
460
+ }
461
+ }
462
+ }
463
+ }
464
+ ```
465
+
466
+ Supported variant names are `string`, `number`, `boolean`, `array`, and `object`.
467
+ Undeclared types, non-finite numbers, excessive bytes, depth or items, and unclassified
468
+ object fields fail closed.
469
+
470
+ ### Reviewed scalar domains
471
+
472
+ Use `enum` when a structural string, number, or boolean must remain exact, but only a
473
+ small reviewed set is valid. For example, an image mode can retain its application
474
+ meaning without allowing arbitrary text through:
475
+
476
+ ```json
477
+ {
478
+ "action": "DERIVE",
479
+ "recipe": {
480
+ "kind": "enum",
481
+ "values": ["contain", "cover", "custom"],
482
+ "allowNull": true
483
+ }
484
+ }
485
+ ```
486
+
487
+ The recipe accepts one through 128 distinct values. Each value must be a JSON string,
488
+ finite number, or boolean; strings are limited to 256 UTF-8 bytes. `null` is accepted
489
+ only when `allowNull` is true. Runtime values match by both type and exact value, so the
490
+ number `1` does not authorize the string `"1"`. Rejected values are never included in
491
+ the error message.
492
+
493
+ An enum can be used directly or inside reviewed objects, arrays, dictionary values, and
494
+ `approved-owner` branches. It validates structure; it is not a place to list private
495
+ data, credentials, tokens, user-entered prose, or identifiers.
496
+
497
+ ### Open structural codes
498
+
499
+ Use `validated-string` when an open set of structural codes must remain exact and an
500
+ enum would incorrectly freeze the domain. The built-in `portable-code` format accepts
501
+ case-preserving ASCII letters, digits, `_`, and `-`, starting with a letter or digit:
502
+
503
+ ```json
504
+ {
505
+ "action": "DERIVE",
506
+ "recipe": {
507
+ "kind": "validated-string",
508
+ "format": "portable-code",
509
+ "maximumBytes": 64,
510
+ "allowNull": true
511
+ }
512
+ }
513
+ ```
514
+
515
+ `maximumBytes` is required and may be 1 through 1,024. Dots, whitespace, slashes,
516
+ colons, control characters, non-ASCII text, wrong JSON types, and over-limit strings
517
+ are rejected without printing the input. This preserves codes such as `PROCNT`, `203`,
518
+ or `energy-kcal_100g`, while refusing JWT-shaped strings. Arbitrary regular expressions
519
+ are not supported.
520
+
521
+ Like enums, validated strings compose inside reviewed objects, arrays, dictionary
522
+ values, and `approved-owner` branches. Use them only for documented structural code
523
+ domains—not prose, URLs, paths, credentials, secrets, personal data, or identifiers
524
+ whose legitimate alphabet requires other characters.
525
+
526
+ ### JSON dictionaries with runtime keys
527
+
528
+ Use `json-dictionary` for a reviewed object whose keys come from runtime data. This is
529
+ different from `json-object`, where every field name is known in advance. A dictionary
530
+ requires a key format, an explicit key action, one value declaration, and item, depth and
531
+ byte bounds:
532
+
533
+ ```json
534
+ {
535
+ "action": "DERIVE",
536
+ "recipe": {
537
+ "kind": "json-dictionary",
538
+ "maximumItems": 100,
539
+ "maximumDepth": 3,
540
+ "maximumBytes": 8192,
541
+ "keys": { "format": "positive-integer", "action": "KEEP" },
542
+ "values": {
543
+ "action": "DERIVE",
544
+ "recipe": {
545
+ "kind": "json-union",
546
+ "maximumItems": 1,
547
+ "variants": { "number": { "action": "KEEP" } }
548
+ }
549
+ }
550
+ }
551
+ }
552
+ ```
553
+
554
+ Key formats are `integer`, `positive-integer`, `uuid`, `identifier`, and bounded `text`.
555
+ Use `integer` when runtime keys may be positive, negative, or zero. It requires inclusive
556
+ safe-integer `minimum` and `maximum` bounds, accepts only canonical decimal keys, and
557
+ preserves their exact spelling:
558
+
559
+ ```json
560
+ {
561
+ "keys": {
562
+ "format": "integer",
563
+ "action": "KEEP",
564
+ "minimum": -9007199254740991,
565
+ "maximum": 9007199254740991
566
+ }
567
+ }
568
+ ```
569
+
570
+ The reviewed range decides whether zero is allowed. Leading zeroes, `-0`, a leading `+`,
571
+ fractions, exponents, unsafe integers, and unrelated text are rejected. Use
572
+ `positive-integer` for canonical decimal keys starting at 1. Text keys must declare
573
+ `maximumLength`. Every key explicitly uses either `KEEP` or `PSEUDONYMIZE`.
574
+ Identity-bearing keys should use `PSEUDONYMIZE` with the ordinary keyed pseudonym recipe:
575
+
576
+ ```json
577
+ {
578
+ "keys": {
579
+ "format": "uuid",
580
+ "action": "PSEUDONYMIZE",
581
+ "recipe": { "format": "uuid", "namespace": "account-id" }
582
+ }
583
+ }
584
+ ```
585
+
586
+ When a dictionary is keyed by the same numeric identity stored in a column or nested
587
+ numeric field, use `format: "integer"` for both pseudonym recipes and reuse the exact
588
+ namespace:
589
+
590
+ ```json
591
+ {
592
+ "keys": {
593
+ "format": "integer",
594
+ "action": "PSEUDONYMIZE",
595
+ "minimum": -9007199254740991,
596
+ "maximum": 9007199254740991,
597
+ "recipe": { "format": "integer", "namespace": "external:food-id" }
598
+ }
599
+ }
600
+ ```
601
+
602
+ Rehearsal validates the object key as canonical decimal text, converts it to its safe
603
+ integer identity, and then applies the keyed mapping. Therefore the source number `203`
604
+ and reviewed dictionary key `"203"` receive the same pseudonym. Positive, negative, and
605
+ reviewed zero identities correspond; namespace changes and privacy-key rotation still
606
+ produce separate mappings. This normalization applies only to explicitly typed
607
+ `integer` dictionary keys. It does not weaken the key syntax or range checks.
608
+
609
+ Invalid keys, duplicate transformed keys, excessive entries, unclassified surrounding
610
+ object fields, and values rejected by the declared value recipe stop preparation. A
611
+ dictionary never infers that runtime keys are safe. Its value declaration also cannot
612
+ retain an unknown JSON shape wholesale; use a bounded union when a scalar value should be
613
+ kept exactly.
46
614
 
47
615
  The package still exports `validateSanitizationCoverage` and
48
616
  `applySanitizationAction` for existing version 1 preparation tools:
@@ -26,6 +26,9 @@ runtime.
26
26
  - successful receipts written only after verification.
27
27
  - separate exact-digest approval for source setup, source retirement, and identity claim;
28
28
  - short-lived source readers limited to reviewed views/columns and deny checks;
29
+ - externally provisioned readers require canonical view-definition receipts,
30
+ `security_barrier`, independent ownership, an exact non-system readable-column surface,
31
+ no write or role-escalation paths, and database-enforced expiration;
29
32
  - bounded read-only extraction with no raw dump or row-value logging;
30
33
  - exhaustive privacy policy version 2 with an owner-only local pseudonym key;
31
34
  - application children receive only explicit local mappings and stay in an owned process
@@ -39,6 +42,11 @@ No single environment variable or config edit can redirect runtime commands to
39
42
  production. Optional source preparation is a separate boundary and never provides a
40
43
  hosted execution mode.
41
44
 
45
+ External source mode verifies access but does not own it. Rehearsal makes no source-side
46
+ changes and removes only its local credential and receipt. The provider owns early
47
+ revocation and the database enforces the reviewed short expiration; Rehearsal never
48
+ reports local cleanup as proof that a provider-owned role was deleted.
49
+
42
50
  ## Identity providers
43
51
 
44
52
  An optional external identity handshake is distinct from database access. Only declared
@@ -0,0 +1,70 @@
1
+ # Stability and the first stable release
2
+
3
+ Rehearsal leaves beta when the workflow it already promises is dependable. Stable
4
+ `0.1.0` does not require every planned database, desktop interface, or paid feature.
5
+
6
+ ## What becomes a compatibility promise
7
+
8
+ Starting with `0.1.0`, Rehearsal treats these documented surfaces as public:
9
+
10
+ - CLI command names, flags, JSON result fields, and exit categories in
11
+ [CLI commands](commands.md);
12
+ - `rehearsal.config.mjs` schema version 1 and the policy formats documented in
13
+ [Configuration](configuration.md), [Sanitization](sanitization.md), and
14
+ [Runtime and identity policies](runtime-policies.md);
15
+ - package entry points and generated declarations listed in
16
+ [TypeScript](typescript.md);
17
+ - supported Node.js, operating-system, Docker, Supabase, and PostgreSQL boundaries in
18
+ [COMPATIBILITY.md](https://github.com/Ddupasquier/rehearsal-db/blob/main/COMPATIBILITY.md);
19
+ - non-destructive ownership, confirmation, credential, and hosted-access guarantees in
20
+ [Security model](security-model.md).
21
+
22
+ Patch releases may fix defects and improve messages without changing those contracts.
23
+ A future breaking contract change requires a version that clearly communicates it under
24
+ Semantic Versioning. Internal source files, terminal decoration, exact prose, temporary
25
+ file names, and undocumented package paths are not compatibility promises.
26
+
27
+ ## What blocks a release candidate
28
+
29
+ An issue blocks the active release candidate when it can cause any of the following within a supported
30
+ configuration:
31
+
32
+ - data loss, credential exposure, hosted-system access, or mutation outside resources
33
+ that Rehearsal can prove it owns;
34
+ - an incorrect baseline, migration set, verification result, or successful status;
35
+ - failure of clean installation, supported-version upgrade, restore, reset, cleanup, or
36
+ interruption recovery;
37
+ - a documented CLI, configuration, policy, or TypeScript contract behaving differently
38
+ from its reference;
39
+ - a supported Node.js, Supabase, PostgreSQL, macOS, or Linux path failing its maintained
40
+ automated gate.
41
+
42
+ Cosmetic improvements, new database families, production-shaped sampling, the VS Code
43
+ extension, desktop work, and commercial capabilities do not block the first stable
44
+ release.
45
+
46
+ ## Promotion sequence
47
+
48
+ 1. Ship beta.18 with only stability, release-safety, upgrade, and documentation work.
49
+ 2. Freeze broad feature development on the release branch.
50
+ 3. Publish a release candidate from protected `main` through the reviewed npm environment.
51
+ 4. Run the complete Rehearsal Test Lab gate twice from clean isolated workspaces.
52
+ 5. Perform one final package-only upgrade in a real consumer. The consumer must use the
53
+ published package rather than duplicate Rehearsal machinery.
54
+ 6. Keep the current candidate available for at least seven calendar days. Restart the
55
+ window if a blocking package change is required. The container-lifecycle correction
56
+ in `0.1.0-rc.2` restarts this observation window.
57
+ 7. Publish `0.1.0` only when no blocking issue remains and the accepted registry artifact
58
+ matches the tested artifact exactly.
59
+
60
+ Test Lab is the primary verification system. A real consumer is the final compatibility
61
+ check, not a replacement for package-owned coverage. Real Google or GitHub consent does
62
+ not need to run on every build unless authentication behavior changes.
63
+
64
+ ## npm channels
65
+
66
+ Beta and release-candidate versions publish under the `beta` tag. Before a stable
67
+ release exists, `latest` follows that reviewed prerelease so the ordinary install command
68
+ remains useful. Once stable `0.1.0` exists, `latest` stays on the stable line and `beta`
69
+ continues to identify the newest prerelease. Stable publication never rewrites the
70
+ `beta` tag.