hecks 2.7.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (463) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +11 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +18 -99
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres.rb +145 -134
  25. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  26. data/lib/hecks/adapters/driven/prism.rb +7 -30
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  31. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  32. data/lib/hecks/adapters/driven/tenant_provisioner.rb +15 -37
  33. data/lib/hecks/adapters/driven.rb +5 -36
  34. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  35. data/lib/hecks/behaviors/dsl.rb +7 -25
  36. data/lib/hecks/behaviors/expectations.rb +32 -142
  37. data/lib/hecks/behaviors/ir.rb +7 -20
  38. data/lib/hecks/behaviors/rspec.rb +6 -17
  39. data/lib/hecks/behaviors/runner.rb +10 -34
  40. data/lib/hecks/behaviors.rb +4 -8
  41. data/lib/hecks/bluebook/aggregate.rb +9 -75
  42. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  43. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  44. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  45. data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
  46. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  47. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  48. data/lib/hecks/bluebook/assembly.rb +6 -45
  49. data/lib/hecks/bluebook/attribute.rb +12 -61
  50. data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
  51. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  52. data/lib/hecks/bluebook/behaviour/chapter.rb +17 -42
  53. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  54. data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
  55. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  56. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  57. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  58. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  59. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  60. data/lib/hecks/bluebook/behaviour/read_model.rb +38 -26
  61. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  62. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  63. data/lib/hecks/bluebook/capabilities.rb +17 -49
  64. data/lib/hecks/bluebook/chapter.rb +10 -48
  65. data/lib/hecks/bluebook/command.rb +23 -81
  66. data/lib/hecks/bluebook/domain_port.rb +14 -63
  67. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  68. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  69. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  70. data/lib/hecks/bluebook/dsl/binding_proxy.rb +17 -65
  71. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
  72. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
  73. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  74. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  75. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  76. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
  77. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  78. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  79. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -189
  80. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  81. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  82. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  83. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  84. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  85. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  86. data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
  87. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  88. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  89. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  90. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  91. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  92. data/lib/hecks/bluebook/dsl/world_builder.rb +19 -106
  93. data/lib/hecks/bluebook/entity.rb +13 -71
  94. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  95. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  96. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  97. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  98. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  99. data/lib/hecks/bluebook/expression/resolver.rb +37 -660
  100. data/lib/hecks/bluebook/hexagon.rb +5 -28
  101. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  102. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  103. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  104. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  105. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  106. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  107. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
  108. data/lib/hecks/bluebook/meta_validator/shapes.rb +54 -268
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +47 -308
  110. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  111. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  112. data/lib/hecks/bluebook/meta_validator.rb +79 -349
  113. data/lib/hecks/bluebook/model_check/client_profile.rb +10 -91
  114. data/lib/hecks/bluebook/model_check.rb +102 -600
  115. data/lib/hecks/bluebook/pattern_subset.rb +11 -69
  116. data/lib/hecks/bluebook/process_manager.rb +13 -83
  117. data/lib/hecks/bluebook/project_loader.rb +3 -39
  118. data/lib/hecks/bluebook/project_register.rb +12 -60
  119. data/lib/hecks/bluebook/query.rb +9 -48
  120. data/lib/hecks/bluebook/read_model.rb +57 -81
  121. data/lib/hecks/bluebook/reference.rb +7 -41
  122. data/lib/hecks/bluebook/smoke_test.rb +8 -98
  123. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  124. data/lib/hecks/bluebook/translation.rb +26 -62
  125. data/lib/hecks/bluebook/value_object.rb +11 -36
  126. data/lib/hecks/bluebook.rb +5 -22
  127. data/lib/hecks/cache_dir.rb +84 -0
  128. data/lib/hecks/cli/document.rb +86 -0
  129. data/lib/hecks/cli/ir.rb +45 -0
  130. data/lib/hecks/cli/mcp.rb +31 -0
  131. data/lib/hecks/cli/mcp_door.rb +366 -0
  132. data/lib/hecks/cli/model_check.rb +203 -0
  133. data/lib/hecks/cli/project_cli.rb +90 -0
  134. data/lib/hecks/cli/project_diagrams.rb +36 -0
  135. data/lib/hecks/cli/run.rb +173 -0
  136. data/lib/hecks/cli/smoke_test.rb +45 -0
  137. data/lib/hecks/cli/stores.rb +53 -0
  138. data/lib/hecks/cli.rb +160 -0
  139. data/lib/hecks/construct.rb +8 -34
  140. data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
  141. data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
  142. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  143. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  144. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  145. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  146. data/lib/hecks/embryonaut_bluebook.rb +4 -71
  147. data/lib/hecks/facade/cli_door.rb +24 -106
  148. data/lib/hecks/facade/cli_runner.rb +28 -172
  149. data/lib/hecks/facade/command_request.rb +13 -33
  150. data/lib/hecks/facade/handle.rb +34 -121
  151. data/lib/hecks/facade/json_door.rb +31 -146
  152. data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
  153. data/lib/hecks/facade/surface/chapter.rb +10 -54
  154. data/lib/hecks/facade/surface.rb +8 -23
  155. data/lib/hecks/facade.rb +10 -25
  156. data/lib/hecks/forms/app.rb +15 -70
  157. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  158. data/lib/hecks/forms/field_renderer.rb +17 -156
  159. data/lib/hecks/forms/field_shape.rb +30 -222
  160. data/lib/hecks/forms/html.rb +12 -47
  161. data/lib/hecks/forms/index_renderer.rb +1 -4
  162. data/lib/hecks/forms/page.rb +3 -9
  163. data/lib/hecks/forms/params.rb +14 -60
  164. data/lib/hecks/forms/port_argument.rb +2 -19
  165. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  166. data/lib/hecks/forms/record_renderer.rb +8 -75
  167. data/lib/hecks/forms/record_table.rb +7 -50
  168. data/lib/hecks/forms/reference_options.rb +3 -30
  169. data/lib/hecks/forms/value_object_shape.rb +8 -35
  170. data/lib/hecks/forms.rb +5 -33
  171. data/lib/hecks/fqn.rb +5 -12
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  175. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  176. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  177. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  178. data/lib/hecks/framework.rb +11 -80
  179. data/lib/hecks/freezer.rb +5 -39
  180. data/lib/hecks/grammar/expression.bluebook +15 -40
  181. data/lib/hecks/grammar/translation.bluebook +3 -6
  182. data/lib/hecks/grammar.rb +16 -41
  183. data/lib/hecks/ir.rb +14 -83
  184. data/lib/hecks/language/adapter.bluebook +2 -7
  185. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  186. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  187. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
  188. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  189. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  190. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  191. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  192. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  193. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  194. data/lib/hecks/language/bluebook/query.bluebook +12 -43
  195. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  196. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  197. data/lib/hecks/language/bluebook/vocabulary.bluebook +98 -197
  198. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  199. data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
  200. data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
  201. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  202. data/lib/hecks/language/oidc.json +35 -0
  203. data/lib/hecks/language/port.bluebook +4 -17
  204. data/lib/hecks/language/translation/translation.bluebook +3 -9
  205. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  206. data/lib/hecks/language/world/wiring.bluebook +1 -3
  207. data/lib/hecks/language/world/world.bluebook +5 -7
  208. data/lib/hecks/literal.rb +8 -50
  209. data/lib/hecks/mcp_door_scope.rb +95 -0
  210. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  211. data/lib/hecks/naming.rb +29 -137
  212. data/lib/hecks/ports/access_control.rb +7 -23
  213. data/lib/hecks/ports/agent/answers.rb +5 -23
  214. data/lib/hecks/ports/agent.rb +47 -172
  215. data/lib/hecks/ports/authentication.rb +12 -39
  216. data/lib/hecks/ports/authorization.rb +16 -43
  217. data/lib/hecks/ports/clock.rb +5 -45
  218. data/lib/hecks/ports/extraction.rb +7 -12
  219. data/lib/hecks/ports/identity_assignment.rb +8 -29
  220. data/lib/hecks/ports/identity_generation.rb +6 -29
  221. data/lib/hecks/ports/identity_resolution.rb +2 -10
  222. data/lib/hecks/ports/key_vault.rb +9 -45
  223. data/lib/hecks/ports/persistence/append_only.rb +62 -65
  224. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  225. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  226. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  227. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  228. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
  229. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
  231. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +7 -26
  232. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
  233. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
  250. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  251. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  252. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  253. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  254. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  255. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  256. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
  257. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +22 -122
  258. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  259. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  260. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  261. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  262. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  263. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  264. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  266. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  267. data/lib/hecks/ports/persistence/state_codec.rb +23 -103
  268. data/lib/hecks/ports/persistence.rb +7 -14
  269. data/lib/hecks/ports/projection.rb +42 -84
  270. data/lib/hecks/ports/query/in_memory.rb +10 -58
  271. data/lib/hecks/ports/query/ordering.rb +11 -32
  272. data/lib/hecks/ports/query.rb +14 -37
  273. data/lib/hecks/ports.rb +2 -8
  274. data/lib/hecks/projections/bootstrap_table.rb +11 -43
  275. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  276. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  277. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  278. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  279. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  280. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  281. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  282. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  283. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  284. data/lib/hecks/projections/deploy/fargate.rb +35 -202
  285. data/lib/hecks/projections/deploy/lambda.rb +151 -679
  286. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  287. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  288. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  289. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  290. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  291. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  292. data/lib/hecks/projections/deploy/preview/template.rb +2 -58
  293. data/lib/hecks/projections/deploy/preview.rb +2 -67
  294. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  295. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  296. data/lib/hecks/projections/deploy/shared.rb +47 -146
  297. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  298. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  299. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  300. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  301. data/lib/hecks/projections/diagrams.rb +20 -582
  302. data/lib/hecks/projections/glossary/html.rb +14 -130
  303. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  304. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  305. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  306. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  307. data/lib/hecks/projections/glossary.rb +30 -222
  308. data/lib/hecks/projections/ir.rb +3 -10
  309. data/lib/hecks/projections/model/deviations.rb +15 -42
  310. data/lib/hecks/projections/model.rb +12 -51
  311. data/lib/hecks/projections/oidc.rb +12 -56
  312. data/lib/hecks/projections/parser_table.rb +9 -28
  313. data/lib/hecks/projections/reference.rb +9 -16
  314. data/lib/hecks/projections/rust_vocabulary.rb +12 -250
  315. data/lib/hecks/projections/shape.rb +3 -29
  316. data/lib/hecks/projections/statements.rb +7 -86
  317. data/lib/hecks/projections/vocabulary.rb +6 -22
  318. data/lib/hecks/projections.rb +2 -16
  319. data/lib/hecks/projector/cli_projector.rb +60 -375
  320. data/lib/hecks/projector/docs_projector.rb +10 -201
  321. data/lib/hecks/projector/exporter.rb +46 -138
  322. data/lib/hecks/projector/ir_projector.rb +2 -9
  323. data/lib/hecks/projector/narrate_projector.rb +15 -189
  324. data/lib/hecks/projector/target.rb +13 -76
  325. data/lib/hecks/projector.rb +30 -121
  326. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  327. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  328. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  329. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  330. data/lib/hecks/query_specification/common/options.rb +2 -6
  331. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  332. data/lib/hecks/query_specification/field_path.rb +37 -77
  333. data/lib/hecks/query_specification/hop_path.rb +12 -76
  334. data/lib/hecks/release/gem_pin.rb +2 -26
  335. data/lib/hecks/release/runner/ci_publisher.rb +93 -0
  336. data/lib/hecks/release/runner/commands.rb +66 -0
  337. data/lib/hecks/release/runner/console.rb +52 -0
  338. data/lib/hecks/release/runner/gem_publisher.rb +43 -0
  339. data/lib/hecks/release/runner/git.rb +48 -0
  340. data/lib/hecks/release/runner/npm_publisher.rb +75 -0
  341. data/lib/hecks/release/runner/preflight.rb +104 -0
  342. data/lib/hecks/release/runner/published.rb +55 -0
  343. data/lib/hecks/release/runner/tagger.rb +80 -0
  344. data/lib/hecks/release/runner.rb +215 -0
  345. data/lib/hecks/rendering.rb +4 -26
  346. data/lib/hecks/router/namespace_installer.rb +12 -36
  347. data/lib/hecks/router.rb +4 -8
  348. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  349. data/lib/hecks/runtime/boot_gates.rb +3 -18
  350. data/lib/hecks/runtime/caller.rb +19 -59
  351. data/lib/hecks/runtime/capability_graph.rb +5 -21
  352. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  353. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  354. data/lib/hecks/runtime/command_interpreter.rb +125 -307
  355. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  356. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  357. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  358. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  359. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  360. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  361. data/lib/hecks/runtime/dispatcher.rb +82 -241
  362. data/lib/hecks/runtime/entity_element.rb +40 -462
  363. data/lib/hecks/runtime/entity_interpreter.rb +65 -220
  364. data/lib/hecks/runtime/errors.rb +32 -115
  365. data/lib/hecks/runtime/event.rb +3 -25
  366. data/lib/hecks/runtime/identity.rb +18 -154
  367. data/lib/hecks/runtime/instance.rb +18 -150
  368. data/lib/hecks/runtime/interpreting.rb +11 -70
  369. data/lib/hecks/runtime/invocation.rb +28 -164
  370. data/lib/hecks/runtime/loader.rb +48 -186
  371. data/lib/hecks/runtime/outbox.rb +38 -261
  372. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  373. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  374. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  375. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  376. data/lib/hecks/runtime/read_model_interpreter.rb +139 -208
  377. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  378. data/lib/hecks/runtime/reference_hop.rb +8 -81
  379. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  380. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  381. data/lib/hecks/runtime/registry/verification.rb +52 -225
  382. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  383. data/lib/hecks/runtime/registry.rb +103 -244
  384. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  385. data/lib/hecks/runtime/routing.rb +4 -15
  386. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  387. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  388. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  389. data/lib/hecks/runtime/tenant_check.rb +6 -68
  390. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  391. data/lib/hecks/runtime/value/admission.rb +16 -67
  392. data/lib/hecks/runtime/value/coercion.rb +155 -641
  393. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  394. data/lib/hecks/runtime/value.rb +12 -84
  395. data/lib/hecks/runtime.rb +17 -65
  396. data/lib/hecks/storehouse.rb +146 -567
  397. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  398. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  399. data/lib/hecks/vendoring/git_environment.rb +2 -8
  400. data/lib/hecks/vendoring/git_source.rb +4 -10
  401. data/lib/hecks/vendoring.rb +7 -46
  402. data/lib/hecks/version.rb +3 -14
  403. data/lib/hecks/vocabulary.rb +5 -0
  404. data/lib/hecks.rb +32 -82
  405. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  406. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  407. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  408. metadata +30 -59
  409. data/lib/hecks/bench/cli.rb +0 -74
  410. data/lib/hecks/bench/environment.rb +0 -100
  411. data/lib/hecks/bench/postgres_probe.rb +0 -41
  412. data/lib/hecks/bench/report.rb +0 -116
  413. data/lib/hecks/bench/ruby_runner.rb +0 -151
  414. data/lib/hecks/bench/run.rb +0 -55
  415. data/lib/hecks/bench/rust_runner.rb +0 -165
  416. data/lib/hecks/bench/stats.rb +0 -75
  417. data/lib/hecks/bench/suite.rb +0 -172
  418. data/lib/hecks/bench/workload.rb +0 -137
  419. data/lib/hecks/bench.rb +0 -28
  420. data/lib/hecks/codemod.rb +0 -443
  421. data/lib/hecks/corpus.rb +0 -479
  422. data/lib/hecks/doc/reference.rb +0 -585
  423. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  424. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  425. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  426. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  427. data/lib/hecks/fuzzing/differential.rb +0 -192
  428. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  429. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  430. data/lib/hecks/fuzzing/form_census.rb +0 -285
  431. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  432. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  433. data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
  434. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  435. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  436. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  437. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  438. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  439. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  440. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  441. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  442. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  443. data/lib/hecks/fuzzing/properties.rb +0 -267
  444. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  445. data/lib/hecks/fuzzing/replay.rb +0 -930
  446. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  447. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  448. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  449. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  450. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  451. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  452. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  453. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  454. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  455. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  456. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  457. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  458. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  459. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  460. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  461. data/lib/hecks/fuzzing.rb +0 -23
  462. data/lib/hecks/grammar/evolve.rb +0 -524
  463. data/lib/hecks/query_ir.rb +0 -482
@@ -1,658 +0,0 @@
1
- require "fileutils"
2
- require "tmpdir"
3
- require "securerandom"
4
-
5
- module Hecks
6
- module Fuzzing
7
- # A fresh, in-process adapter for every ephemeral boot.
8
- #
9
- # ## Why rebind at all
10
- #
11
- # Fuzzing/replay copies a domain to a tmpdir and boots from there
12
- # specifically to get zero-history state — `rm_rf`ing the copy's own
13
- # `data/` achieves that for a file-based adapter (Memory,
14
- # SqlitePersistence, Heki), because copying the directory copies the
15
- # store. It achieves nothing for an adapter that lives outside the
16
- # copied directory entirely — Postgres, named by a fixed connection
17
- # string in `.world` (examples/pizzas/bluebook/pizzas.world, for
18
- # instance). Copying the directory does not copy or isolate the
19
- # database, so a "fresh" boot against a Postgres-bound domain would
20
- # still see every record any other run, ever, wrote to it.
21
- #
22
- # So every `.hecksagon` in the copy gets its persistence binding
23
- # rewritten to Memory before booting, and any `projected_by` bind
24
- # dropped outright (optional — `Registry#read_repository` already
25
- # falls back to the authoritative repository when none exists). The
26
- # domain's own rules and shape are untouched ; only which adapter this
27
- # one ephemeral copy answers through changes. What the domain is
28
- # bound to for real deployment is never touched — only this tmp copy.
29
- #
30
- # ## `adapter:` modes
31
- #
32
- # `adapter:` (PRD 02) picks which real persistence one ephemeral boot
33
- # answers through. `:memory` is the default and the only mode every
34
- # caller that passes no `adapter:` still gets. `:sqlite` rebinds to
35
- # the real SQLite adapter instead of the in-memory one: 15 declared
36
- # properties (properties.rb) and every fuzz/replay run only ever
37
- # exercised Memory's own hand-written repository until this mode
38
- # existed, and `spec/adapters/query_agreement_spec.rb` already found 4
39
- # shipped query bugs from exactly that comparison, on a fixed corpus
40
- # far smaller than what the fuzzer generates. `:sqlite` rewrites to
41
- # `"SqlitePersistence"` — the name
42
- # `lib/hecks/adapters/driven/sqlite.adapter` actually registers under
43
- # (`Sqlite` is the class; `SqlitePersistence` is a thin subclass
44
- # that's the one real port binding names) — and needs no `.world`
45
- # settings at all, the same reason Memory needs none:
46
- # `Sqlite#resolve_path` already defaults an
47
- # unbound `database` setting to `"data/<table>.db"` relative to
48
- # `root:`, which `Hecks.boot(copy)` passes as this ephemeral copy's
49
- # own directory — a fresh, empty `data/` per run, exactly like
50
- # Memory's own zero-history guarantee, just backed by a real SQLite
51
- # file instead of a Hash. `:postgres` (PRD 02, docs/prds/02-fuzzer-
52
- # real-adapters.md) and `:postgres_era` (`rebind_to_postgres_era!`'s
53
- # own header, below) are the two real-Postgres modes. Each writes its
54
- # own fresh `.world` per `.hecksagon` rather than relying on the
55
- # zero-config default Sqlite/Memory get, and each needs a real,
56
- # reachable Postgres server: `:postgres` sources one shared,
57
- # permanent scratch database/schema this module itself owns (see
58
- # `FUZZ_POSTGRES_DATABASE`'s own header); `:postgres_era` instead
59
- # requires the caller to supply (and own the lifecycle of) its own
60
- # throwaway `database:`/`schema:`, since its only caller
61
- # (`bin/qa_sweep --persistence-parity`) already has to manage a
62
- # disposable database of its own, never a shared one this module could
63
- # safely default to.
64
- module IsolatedBoot
65
- module_function
66
-
67
- # PRD 02 (docs/future-features.md) — `adapter:` picks which real
68
- # persistence this one ephemeral boot answers through, not just
69
- # Memory. `:memory` is the original, zero-config behavior (every
70
- # other caller in this codebase that doesn't pass `adapter:` gets
71
- # exactly what it always got). `:sqlite` is nearly as cheap —
72
- # `Adapters::Sqlite#resolve_path` already defaults to
73
- # `data/<table>.db` under the copy's own root with no settings at
74
- # all, the same "needs nothing declared" property Memory has, so a
75
- # real on-disk B-tree gets exercised for free. `:postgres` is the
76
- # expensive one — a real server, a real round trip per dispatch —
77
- # so `bin/fuzz --adapter postgres` is meant to run with smaller
78
- # seed/step counts than the Memory default, not as a like-for-like
79
- # swap; see that flag's own comment.
80
- # `database:`/`schema:` are only meaningful for `adapter: :postgres_era`
81
- # — see `rebind_to_postgres_era!`'s own header for why that mode takes
82
- # caller-supplied connection identity instead of a hardcoded shared
83
- # constant the way `:postgres` does. Every other adapter ignores both;
84
- # accepting them unconditionally here (rather than a separate method
85
- # signature per adapter) keeps `SequenceGenerator`/`Replay`'s own
86
- # single passthrough (`adapter:`, now joined by these two) uniform
87
- # across all four modes.
88
- #
89
- # @param domain_path [String] path to the real domain directory to copy and isolate
90
- # @param adapter [Symbol] which persistence the copy answers through: `:memory`
91
- # (default), `:sqlite`, `:postgres`, or `:postgres_era`
92
- # @param database [String, nil] the database name, required only for
93
- # `adapter: :postgres_era`; ignored by every other adapter
94
- # @param schema [String, nil] the schema name, required only for
95
- # `adapter: :postgres_era`; ignored by every other adapter
96
- # @param scratch [Hash{Symbol => String}] `:database` and/or `:schema` for
97
- # `adapter: :postgres` to use instead of the shared fuzz scratch schema, so a caller
98
- # such as `bin/bench` never drops the schema a concurrent `bin/fuzz` is using;
99
- # ignored by every other adapter
100
- # @yield [String] the freshly rebound copy's root directory, ready to boot
101
- # @yieldreturn [Object] anything; becomes this method's own return value
102
- # @return [Object] whatever the given block returns
103
- # @raise [ArgumentError] if `adapter` is not `:memory`, `:sqlite`, `:postgres`, or
104
- # `:postgres_era`, or if `adapter: :postgres_era` is given without both
105
- # `database:` and `schema:`
106
- def call(domain_path, adapter: :memory, database: nil, schema: nil, scratch: {})
107
- Dir.mktmpdir("hecks-fuzz") do |tmp|
108
- copy = File.join(tmp, File.basename(domain_path))
109
- copy_dereferencing(domain_path, copy)
110
- FileUtils.rm_rf(File.join(copy, "data"))
111
- case adapter
112
- when :memory then rebind_to_memory!(copy)
113
- when :sqlite then rebind_to_sqlite!(copy)
114
- when :postgres then rebind_to_postgres!(copy, **scratch)
115
- when :postgres_era then rebind_to_postgres_era!(copy, database: database, schema: schema)
116
- else raise ArgumentError,
117
- "unknown fuzz adapter #{adapter.inspect} — :memory, :sqlite, :postgres, or :postgres_era"
118
- end
119
- yield copy
120
- end
121
- end
122
-
123
- # Symlinks are followed, not copied. `FileUtils.cp_r` reproduces a
124
- # symlink as a symlink, and a relative one then points at nothing
125
- # from a tmpdir — `lib/hecks/framework/bluebook/compliance
126
- # .bluebook` is exactly that, a link to
127
- # `examples/compliance/bluebook/compliance.bluebook`, so the whole
128
- # framework domain failed to boot here with a LoadError naming a
129
- # path under /var/folders that had never existed. Nothing about the
130
- # domain was wrong; the copy was.
131
- #
132
- # `FileUtils.cp` follows a symlink and copies its content, which is
133
- # what an isolated boot wants: the copy has to stand alone, since
134
- # rebind! rewrites files in it and must not reach back through a
135
- # link into the real tree.
136
- #
137
- # The vendored bluebook packages the domain's hecksagons name with
138
- # `uses_embryonaut_bluebook` are carried too — see `carry_vendored_bluebooks!`.
139
- #
140
- # @param source [String] the real domain directory to copy
141
- # @param destination [String] the tmpdir path to copy it into; created if missing
142
- # @return [void]
143
- def copy_dereferencing(source, destination)
144
- copy_files(source, destination)
145
- carry_vendored_bluebooks!(source, destination)
146
- end
147
-
148
- # Copies every file under `source` into `destination`, following symlinks.
149
- #
150
- # @param source [String] the directory to copy
151
- # @param destination [String] the path to copy it into; created if missing
152
- # @return [void]
153
- def copy_files(source, destination)
154
- FileUtils.mkdir_p(destination)
155
- Dir.glob(File.join(source, "**", "*"), File::FNM_DOTMATCH).each do |path|
156
- next if [".", ".."].include?(File.basename(path))
157
-
158
- target = File.join(destination, path.delete_prefix("#{source}/"))
159
- if File.directory?(path)
160
- FileUtils.mkdir_p(target)
161
- else
162
- FileUtils.mkdir_p(File.dirname(target))
163
- begin
164
- FileUtils.cp(path, target)
165
- rescue Errno::ENOENT
166
- # A transient sibling (an adapter's atomic-write `.tmp.<pid>`
167
- # file, caught mid-rename by the glob above) can vanish
168
- # between listing and copy — under parallel_r-spec two
169
- # workers share the real example tree. A file that no longer
170
- # exists was never part of the state this copy needs.
171
- end
172
- end
173
- end
174
- end
175
-
176
- # Copies the vendored bluebook packages the copy's hecksagons name into
177
- # the place a boot of the copy looks for them.
178
- #
179
- # `Hecks.boot` roots a registry at the parent of the directory it boots,
180
- # and `uses_embryonaut_bluebook "<name>"` loads from
181
- # `<root>/vendor/embryonaut_bluebooks/<name>/bluebook` (see
182
- # `EmbryonautBluebook.load!`). The copy holds only the domain directory,
183
- # so without this a target that vendors a package crashed the boot with
184
- # "no vendored embryonaut bluebook named ...". Only the packages a
185
- # hecksagon names are copied, never the rest of `vendor/`. A package the
186
- # source root does not have is skipped, so the boot's own error stands.
187
- #
188
- # @param source [String] the real domain directory
189
- # @param destination [String] the copy's directory; its parent is the copy's root
190
- # @return [void]
191
- def carry_vendored_bluebooks!(source, destination)
192
- names = Dir.glob(File.join(destination, "**", "*.hecksagon")).flat_map do |path|
193
- File.read(path).scan(/uses_embryonaut_bluebook\s*\(?\s*"([\w-]+)"/).flatten
194
- end
195
- names.uniq.each do |name|
196
- package = File.join(File.dirname(source), "vendor", "embryonaut_bluebooks", name)
197
- next unless File.directory?(package)
198
-
199
- copy_files(package, File.join(File.dirname(destination), "vendor", "embryonaut_bluebooks", name))
200
- end
201
- end
202
-
203
- # Rewrites every `.hecksagon` in the copy to bind through Memory and replaces its
204
- # `.world` files with a `default_adapter "Memory"` one, so the boot needs no settings.
205
- #
206
- # @param copy [String] the isolated copy's root directory
207
- # @return [void]
208
- def rebind_to_memory!(copy)
209
- rewrite_bindings!(copy, "Memory")
210
- strip_translations!(copy)
211
-
212
- # **The settings, not just the bind** — `WorldBuilder#method_missing`
213
- # stores a settings block under both "verb:adapter" and the bare
214
- # "verb" (world_builder.rb:32-33), so a bind rewritten to Memory
215
- # still falls back to whatever adapter's settings were declared
216
- # bare — Postgres's `database:`, which Memory does not take and
217
- # WiringError refuses on sight. Both Memory and Sqlite need no
218
- # settings at all (`Sqlite#resolve_path` defaults `database` to
219
- # `data/<table>.db` under `root:` when unset — the same
220
- # zero-config default Memory gets from having no `.world` at
221
- # all), so the simplest correct fix for either target is
222
- # dropping the shipped `.world` from the copy. What replaces it is
223
- # only a `default_adapter`, so an aggregate the hecksagon leaves
224
- # unbound (the real world's `default_adapter` bound it) still lands on
225
- # this mode's adapter — see `write_default_worlds!`.
226
- write_default_worlds!(copy, "Memory")
227
- end
228
-
229
- # Same dance as memory, one adapter over — `Adapters::Sqlite#
230
- # resolve_path` (adapters/driven/sqlite.rb) defaults to
231
- # `data/<table>.db` under the boot's own root when no `database`
232
- # setting is declared, which `data/` already being cleared makes a
233
- # fresh on-disk store with zero configuration, the same "no .world
234
- # needed" property Memory has. `data/` itself is left for Sqlite to
235
- # recreate on first write, same as it always was for Memory/Heki —
236
- # nothing here creates it up front.
237
- #
238
- # @param copy [String] the isolated copy's root directory
239
- # @return [void]
240
- def rebind_to_sqlite!(copy)
241
- rewrite_bindings!(copy, "SqlitePersistence")
242
- strip_translations!(copy)
243
- write_default_worlds!(copy, "SqlitePersistence")
244
- end
245
-
246
- # **The expensive one** — Postgres has no zero-config default the way
247
- # Sqlite/Memory do (`Adapters::Postgres.connect_for` refuses outright
248
- # with no `database` setting), so dropping `.world` the way the
249
- # other two do would just move the WiringError from "wrong adapter"
250
- # to "no adapter." Every domain name this copy declares gets a
251
- # fresh `.world` written for it instead — not a rewrite of whatever
252
- # was there, a replacement, same reasoning `rewrite_bindings!`
253
- # already applies to `.hecksagon`: this ephemeral boot owns every
254
- # binding decision, nothing about a real deployment's own settings
255
- # is relevant or safe to half-preserve here.
256
- #
257
- # One shared schema, dropped and recreated before every boot — not a
258
- # fresh randomly-named one per call. `bin/fuzz` drives every
259
- # ephemeral boot sequentially (one `IsolatedBoot.call` fully exits
260
- # before the next begins — see that file's own single-threaded
261
- # `while` loop), so nothing is ever concurrent here ; a fresh name
262
- # every time would just leak schemas in `FUZZ_POSTGRES_DATABASE`
263
- # forever with nothing to ever drop them. If a caller ever does
264
- # start running fuzz adapters concurrently, this needs to move to a
265
- # process-unique schema name (`SecureRandom.hex` is already
266
- # `require`d here for exactly that day) — flagged, not solved,
267
- # since nothing today calls this from more than one thread.
268
- FUZZ_POSTGRES_DATABASE = "hecks_fuzz".freeze
269
- FUZZ_POSTGRES_SCHEMA = "hecks_fuzz".freeze
270
-
271
- # Rewrites every `.hecksagon` in the copy to bind through Postgres, against the
272
- # shared `FUZZ_POSTGRES_DATABASE`/`FUZZ_POSTGRES_SCHEMA` scratch schema unless the
273
- # caller names its own, which lets `bin/bench` run without touching the fuzzer's.
274
- #
275
- # @param copy [String] the isolated copy's root directory
276
- # @param database [String, nil] the scratch database, `FUZZ_POSTGRES_DATABASE` when nil
277
- # @param schema [String, nil] the scratch schema, `FUZZ_POSTGRES_SCHEMA` when nil
278
- # @return [void]
279
- def rebind_to_postgres!(copy, database: nil, schema: nil)
280
- require "pg"
281
- database ||= FUZZ_POSTGRES_DATABASE
282
- schema ||= FUZZ_POSTGRES_SCHEMA
283
- rewrite_bindings!(copy, "Postgres")
284
- strip_translations!(copy)
285
- ensure_fuzz_schema!(database, schema)
286
-
287
- # One `.world` per directory a `.hecksagon` actually lives in, not
288
- # one at `copy`'s own root — `Folder#load_domain` resolves a
289
- # single `bluebook_directory` and globs `*.world` there, non-
290
- # recursively (`Folder#load_each`); a domain can hold several
291
- # `Hecks.hecksagon "<Name>" do ... end` sibling blocks in that one
292
- # file (banking.hecksagon declares "Banking", "Governance", and
293
- # "Identity" together), so every name found in one directory
294
- # is bundled into one `.world` written there, not scattered by name.
295
- write_worlds!(copy, "hecks_fuzz_postgres.world") do |name|
296
- <<~WORLD
297
- Hecks.world "#{name}" do
298
- default_adapter "Postgres"
299
- persisted_by("Postgres") do
300
- database "#{database}"
301
- schema "#{schema}"
302
- end
303
- end
304
- WORLD
305
- end
306
- write_unbound_chapter_worlds!(copy)
307
- end
308
-
309
- # Admin connection lives outside the tmp copy entirely — same as
310
- # every other real-Postgres spec in this repo (`support/
311
- # postgres_probe.rb`'s own header). Database created once per
312
- # process and remembered (`@fuzz_database_ready` on this module's
313
- # own singleton, the same memoization shape `PostgresProbe
314
- # .available?` already uses) ; the schema inside it is dropped and
315
- # recreated on every call, which is what actually isolates one
316
- # ephemeral boot's data from the next.
317
- #
318
- # @param database [String] the scratch database to create if missing
319
- # @param schema [String] the schema to drop and recreate inside it
320
- # @return [void]
321
- def ensure_fuzz_schema!(database = FUZZ_POSTGRES_DATABASE, schema = FUZZ_POSTGRES_SCHEMA)
322
- # `Adapters::Postgres#initialize` opens one real `PG::Connection`
323
- # per aggregate and never explicitly closes it — fine for a
324
- # process that boots once and runs, exactly what every other
325
- # caller of this adapter is. A fuzz run boots dozens to hundreds
326
- # of ephemeral times in one process (every seed does at least a
327
- # generate + a replay, `replay_is_deterministic` doubles that,
328
- # shrinking multiplies it further), and `PG::Connection` only
329
- # actually closes its socket when Ruby's GC finalizes the
330
- # (by-then-unreferenced) object — which, under a tight loop like
331
- # this, does not reliably keep pace with how fast new
332
- # connections open. Discovered live: `bin/fuzz --adapter
333
- # postgres` against `examples/banking` hit a real local
334
- # Postgres's own `max_connections` after roughly a dozen
335
- # ephemeral boots, `PG::ConnectionBad: ... "too many clients
336
- # already"`. A `GC.start` here — right before the next ephemeral
337
- # boot's connections open, not on some timer — reclaims every
338
- # connection the previous boot's now-unreferenced adapters held,
339
- # keeping the live count bounded regardless of run length. This
340
- # is a real constraint on running Postgres in a loop, not
341
- # something to route around by connecting less carefully.
342
- GC.start
343
-
344
- @fuzz_databases_ready ||= {}
345
- unless @fuzz_databases_ready[database]
346
- admin = PG.connect(dbname: "postgres")
347
- exists = admin.exec_params(
348
- "SELECT 1 FROM pg_database WHERE datname = $1", [database]
349
- ).ntuples.positive?
350
- admin.exec(%(CREATE DATABASE "#{database}")) unless exists
351
- admin.close
352
- @fuzz_databases_ready[database] = true
353
- end
354
-
355
- db = PG.connect(dbname: database)
356
- # **Quiet on purpose** — same as `Adapters::Postgres.connect_for`'s
357
- # own `SET client_min_messages`: a `DROP SCHEMA ... CASCADE` that
358
- # actually has something to drop (every boot after the first)
359
- # NOTICEs once per dropped object, which is the ordinary case
360
- # here, not news, and would otherwise bury `bin/fuzz`'s own
361
- # output under a wall of "drop cascades to table ..." on every
362
- # single ephemeral boot.
363
- db.exec("SET client_min_messages = warning")
364
- quoted = db.quote_ident(schema)
365
- db.exec("DROP SCHEMA IF EXISTS #{quoted} CASCADE")
366
- db.exec("CREATE SCHEMA #{quoted}")
367
- db.close
368
- end
369
-
370
- # The adapter `:postgres` never touches — `Postgres` and `PostgresEra`
371
- # are sibling, not interchangeable, adapters (see postgres_era.rb's
372
- # own header: "the only one that declares the LINEAGE capability").
373
- # PRD 02 (docs/prds/02-fuzzer-real-adapters.md) shipped `:postgres`
374
- # and explicitly scoped `PostgresEra` out: "nothing here touches
375
- # era/lineage machinery." That gap is real, not cosmetic —
376
- # `examples/directory` (a `compute`/`rekey` translation edge, the
377
- # one domain in this corpus that actually exercises PostgresEra-
378
- # bound SQL compilation) had to be shelved out of `hecks_qa`'s own
379
- # rotation for exactly this reason: every existing fuzz/replay path,
380
- # `:memory` included, structurally cannot reach it. This mode closes
381
- # that — `bin/qa_sweep --persistence-parity` is its first caller.
382
- #
383
- # No shared constant database, unlike `:postgres` above — deliberate.
384
- # `rebind_to_postgres!`'s own `FUZZ_POSTGRES_DATABASE`/`_SCHEMA` are
385
- # module-level constants because `bin/fuzz --adapter postgres` is a
386
- # general-purpose, run-it-anytime tool with no caller-tracked
387
- # lifecycle of its own. This mode's only caller
388
- # (`bin/qa_sweep --persistence-parity`) is different: it dispatches
389
- # through `QualityControl::Target.claim!`'s own cross-process lock
390
- # first (see that script's own header), so at most one sweep is ever
391
- # touching a given target's own disposable database at a time — but
392
- # the caller, not this module, is what owns that database's name and
393
- # lifecycle (created, and genuinely dropped, by the caller itself),
394
- # exactly the discipline `spec/support/qa_sweep_all_fixture.rb`'s own header
395
- # describes and this repository's persistence-parity work is
396
- # required to follow. Accepting `database:`/`schema:` as required
397
- # keyword arguments (never a fallback constant) is what keeps that
398
- # ownership from silently drifting back onto this module the way
399
- # `:postgres`'s own `FUZZ_POSTGRES_DATABASE` already has.
400
- #
401
- # @param copy [String] the isolated copy's root directory
402
- # @param database [String] the caller-owned throwaway database name
403
- # @param schema [String] the caller-owned throwaway schema name
404
- # @return [void]
405
- # @raise [ArgumentError] if `database` or `schema` is empty
406
- def rebind_to_postgres_era!(copy, database:, schema:)
407
- require "pg"
408
- if database.to_s.empty? || schema.to_s.empty?
409
- raise ArgumentError,
410
- "adapter: :postgres_era requires both database: and schema: — a throwaway database/schema " \
411
- "THIS CALLER creates and drops itself (see rebind_to_postgres_era!'s own header). " \
412
- "There is no shared default, unlike :postgres, so the caller cannot forget to own the lifecycle."
413
- end
414
-
415
- rewrite_bindings!(copy, "PostgresEra")
416
- ensure_postgres_era_schema!(database: database, schema: schema)
417
-
418
- # Same one-`.world`-per-`.hecksagon`-directory shape `rebind_to_
419
- # postgres!` uses, for the identical reason (`Folder#
420
- # load_domain` globs `*.world` non-recursively) — see
421
- # `write_worlds!`. `PostgresEra`
422
- # additionally takes `schema:` (postgres_era.rb's own "shared-
423
- # instance isolation" comment): the caller-supplied throwaway
424
- # schema is what actually isolates this one ephemeral boot from
425
- # the next, the same job `FUZZ_POSTGRES_SCHEMA` does for `:postgres`
426
- # — `connect_for` itself idempotently `CREATE SCHEMA IF NOT
427
- # EXISTS`s it, so this method only ever needs to drop it first
428
- # (in `ensure_postgres_era_schema!`, below) for the zero-history
429
- # guarantee every other adapter mode already gives.
430
- #
431
- # `allow_superuser true` — on the record, on purpose. A bare
432
- # `database` connects as the ambient Postgres user, and
433
- # PostgresEra refuses to boot at all when that user is a
434
- # superuser (its era write-fence is row-level security, which a
435
- # superuser walks through — `Lineage#check_fence_applies!`,
436
- # BUG#24). That refusal protects a real ledger from an old
437
- # checkout's stale writes; nothing here is one. This is an
438
- # ephemeral boot into a throwaway schema the caller itself
439
- # creates and drops, whose data no second checkout ever shares,
440
- # and what it compares is Memory's answers against PostgresEra's
441
- # own SQL — the era fence is not under test and cannot be
442
- # crossed. So opt in explicitly rather than make every
443
- # persistence-parity run first provision a fenced role for a
444
- # database it is about to throw away; the one-line warning
445
- # PostgresEra prints per boot under the opt-in is the honest
446
- # price. Inert on a machine whose ambient user is ordinary.
447
- write_worlds!(copy, "hecks_fuzz_postgres_era.world") do |name|
448
- <<~WORLD
449
- Hecks.world "#{name}" do
450
- default_adapter "PostgresEra"
451
- persisted_by("PostgresEra") do
452
- database "#{database}"
453
- schema "#{schema}"
454
- allow_superuser true
455
- end
456
- end
457
- WORLD
458
- end
459
- write_unbound_chapter_worlds!(copy)
460
- end
461
-
462
- # Replaces every `.world` in the copy with one that declares only `default_adapter`,
463
- # so the copy needs no settings and an aggregate its hecksagon leaves unbound
464
- # (a chapter whose real world bound it through `default_adapter`) persists through
465
- # `adapter_name` like every bound one.
466
- #
467
- # @param copy [String] the isolated copy's root directory
468
- # @param adapter_name [String] the persistence adapter every unbound aggregate falls
469
- # back to, such as `"Memory"` or `"SqlitePersistence"`
470
- # @return [void]
471
- def write_default_worlds!(copy, adapter_name)
472
- write_worlds!(copy, "hecks_fuzz_default.world") do |name|
473
- <<~WORLD
474
- Hecks.world "#{name}" do
475
- default_adapter "#{adapter_name}"
476
- end
477
- WORLD
478
- end
479
- end
480
-
481
- # Gives every chapter no hecksagon names a world of its own that falls back to Memory.
482
- #
483
- # A chapter with a bluebook and no hecksagon block (the framework's Privacy) has no
484
- # bind, and the worlds `write_worlds!` wrote declare a `default_adapter` that needs a
485
- # `database`: left alone, the chapter resolves that adapter through the project's
486
- # world with no connection and the boot refuses (`WiringError ... needs a database
487
- # connection`). It persisted through Memory before those worlds carried a
488
- # `default_adapter`, and a chapter's own world's default wins over the project's, so
489
- # this keeps it there.
490
- #
491
- # @param copy [String] the isolated copy's root directory
492
- # @return [void]
493
- def write_unbound_chapter_worlds!(copy)
494
- named = Dir.glob(File.join(copy, "**", "*.hecksagon")).flat_map do |path|
495
- File.read(path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten
496
- end
497
- bluebooks = Dir.glob(File.join(copy, "**", "*.bluebook")).group_by { |path| File.dirname(path) }
498
- bluebooks.each do |dir, files|
499
- unbound = files.flat_map { |path| File.read(path).scan(/Hecks\.bluebook[\s(]+"([^"]+)"/).flatten }
500
- .uniq - named
501
- named.concat(unbound)
502
- next if unbound.empty?
503
-
504
- worlds = unbound.map { |name| %(Hecks.world "#{name}" do\n default_adapter "Memory"\nend\n) }
505
- File.write(File.join(dir, "hecks_fuzz_unbound.world"), worlds.join("\n"))
506
- end
507
- end
508
-
509
- # Writes one `.world` file per directory that holds a `.hecksagon`, naming every
510
- # `Hecks.hecksagon` block found in that directory, and deletes every other `.world`
511
- # in the copy.
512
- #
513
- # One world file per directory, every hecksagon name in that directory — never one
514
- # write per `*.hecksagon` file. A domain's `context_map.hecksagon` sits beside its
515
- # main hecksagon (the `qa` domain declares Governance in one and QualityControl in
516
- # the other), and a second `File.write` to the same world path would drop the first
517
- # file's names, leaving that domain bound to Postgres with no `database` to open.
518
- #
519
- # Any `.world` the copy shipped with (a real deployment's own connection string) is
520
- # redundant with, and would conflict with (`Registry#add_world`'s own header, on
521
- # loading the same domain name twice), the fresh one written here, so it goes the same
522
- # way `rebind_to_memory!` sends every `.world` in a Memory-mode boot.
523
- #
524
- # @param copy [String] the isolated copy's root directory
525
- # @param world_file [String] the basename of the world file written in each directory
526
- # @yield [name] builds the world text for one hecksagon name
527
- # @yieldparam name [String] a domain name a `Hecks.hecksagon` block in the directory declares
528
- # @yieldreturn [String] the `Hecks.world` block for that name
529
- # @return [void]
530
- def write_worlds!(copy, world_file, &world_for)
531
- names_by_dir = Hash.new { |hash, dir| hash[dir] = [] }
532
- Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |hecksagon_path|
533
- names = File.read(hecksagon_path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten
534
- names_by_dir[File.dirname(hecksagon_path)].concat(names)
535
- end
536
- names_by_dir.each do |dir, names|
537
- next if names.empty?
538
-
539
- File.write(File.join(dir, world_file), names.uniq.map(&world_for).join("\n"))
540
- end
541
-
542
- Dir.glob(File.join(copy, "**", "*.world")).each do |path|
543
- File.delete(path) unless File.basename(path) == world_file
544
- end
545
- end
546
-
547
- # The zero-history guarantee for this mode — `DROP SCHEMA ... CASCADE`
548
- # before every ephemeral boot, mirroring `ensure_fuzz_schema!` above
549
- # (same `GC.start`-before-connecting fix for the identical
550
- # `max_connections` exhaustion that method's own comment documents —
551
- # `PostgresEra` opens real `PG::Connection`s exactly like `Postgres`
552
- # does, same unclosed-until-GC'd lifetime). The database itself is
553
- # created here too, idempotently (`CREATE DATABASE IF NOT EXISTS`
554
- # has no Postgres spelling, hence the existence check) — but never
555
- # dropped here: this module creates it once per process because
556
- # `Hecks.boot` needs it to exist before `PostgresEra.connect_for`'s
557
- # own `PG.connect(dbname: ...)` can succeed at all, but dropping it
558
- # again is the caller's own job (its name and lifecycle belong to
559
- # the caller — see `rebind_to_postgres_era!`'s own header), not
560
- # something this per-ephemeral-boot helper should ever do mid-sweep.
561
- #
562
- # @param database [String] the caller-owned database name, created if it does not
563
- # already exist
564
- # @param schema [String] the caller-owned schema name, dropped and recreated
565
- # @return [void]
566
- def ensure_postgres_era_schema!(database:, schema:)
567
- GC.start
568
-
569
- admin = PG.connect(dbname: "postgres")
570
- exists = admin.exec_params(
571
- "SELECT 1 FROM pg_database WHERE datname = $1", [database]
572
- ).ntuples.positive?
573
- admin.exec(%(CREATE DATABASE "#{database}")) unless exists
574
- admin.close
575
-
576
- db = PG.connect(dbname: database)
577
- db.exec("SET client_min_messages = warning")
578
- quoted = db.quote_ident(schema)
579
- db.exec("DROP SCHEMA IF EXISTS #{quoted} CASCADE")
580
- db.close
581
- end
582
-
583
- # The shared rewrite — factored out of `rebind_to_memory!` when
584
- # Sqlite/Postgres modes needed the identical `.hecksagon` surgery
585
- # with only the target adapter name differing. `persisted_by`/
586
- # `projected_by` can be spelled two ways: aggregate-scoped
587
- # (`Banking::Customer.persisted_by("Heki")`, always parenthesised)
588
- # and, since §0's domain-level default binds, a bare call at the
589
- # hecksagon's own root (`persisted_by "Heki"`, no parens, no
590
- # receiver). Both must be caught here or a domain that only
591
- # declares the bare form keeps its real adapter under an
592
- # "isolated" fuzz boot. `projected_by` is dropped outright rather
593
- # than rebound, for every target adapter — `Registry
594
- # #read_repository` already falls back to the authoritative
595
- # repository when none exists, so a read model this ephemeral copy
596
- # never wires is simply unread, not broken.
597
- # A `compute`/`rekey` translation edge refuses to boot at all under
598
- # any non-lineage-capable adapter — found live, wiring this very
599
- # mode up against `examples/directory`: `Runtime::EraCheck
600
- # .check_compute_rules!` (era_check.rb) runs unconditionally for
601
- # every loaded bluebook once the era plugin is loaded at all
602
- # (`bin/qa_sweep`'s own top-of-file `require "hecks/ports/
603
- # persistence/plugins/era"`, needed for the ledger's own
604
- # PostgresEra-bound aggregates), and refuses outright — "compute
605
- # rules require the Postgres adapter" — for any aggregate whose
606
- # lineage carries a `compute` rule and whose bound adapter is not
607
- # lineage-capable. `PostgresEra` is the only adapter that answers
608
- # `lineage_capable? == true` (postgres_era.rb's own `self.
609
- # lineage_capable? = true`) — plain `Postgres` does not, so this
610
- # refusal was already real for `:postgres`/`:sqlite`/`:memory`
611
- # alike, for any domain with a translation edge, before this
612
- # mode's own `:postgres_era` ever existed. This is very likely the
613
- # mechanical reason `examples/directory` had to be shelved out of
614
- # `hecks_qa`'s own rotation in the first place — not merely "less
615
- # interesting to fuzz on Memory," but "cannot boot on Memory at
616
- # all" once the era plugin is loaded, which every real
617
- # `bin/qa_sweep` invocation already does.
618
- #
619
- # The fix is to drop the edge, not to chase the refusal — an
620
- # ephemeral, zero-history replay boot (every mode `IsolatedBoot`
621
- # offers) never has a pre-existing era-1 row to translate in the
622
- # first place, so the translation edge is irrelevant to anything a
623
- # fuzz/replay run actually exercises (ordinary command dispatch
624
- # against a fresh boot) — it only ever matters at mint time,
625
- # against a real, pre-existing database
626
- # (`PostgresEra::LineageManager.check!`, a wholly separate,
627
- # human-approved path this harness was never meant to reach).
628
- # Dropping it here is exactly the same move `rebind_to_memory!`
629
- # already makes for `.world` (irrelevant/conflicting settings for
630
- # an ephemeral boot, deleted outright) — never called for
631
- # `:postgres_era` itself, where the bound adapter genuinely is
632
- # lineage-capable and the edge causes no refusal to begin with.
633
- #
634
- # @param copy [String] the isolated copy's root directory
635
- # @return [void]
636
- def strip_translations!(copy)
637
- Dir.glob(File.join(copy, "**", "translations", "*.bluebook")).each { |path| File.delete(path) }
638
- end
639
-
640
- # Rewrites every `persisted_by` bind — aggregate-scoped or bare at the
641
- # hecksagon's own root, naming its adapter as a literal or through a local
642
- # variable — in the copy's `.hecksagon` files to name `adapter_name`,
643
- # and drops every `projected_by` bind outright.
644
- #
645
- # @param copy [String] the isolated copy's root directory
646
- # @param adapter_name [String] the port binding name to rewrite every
647
- # `persisted_by` to, such as `"Memory"` or `"SqlitePersistence"`
648
- # @return [void]
649
- def rewrite_bindings!(copy, adapter_name)
650
- Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |path|
651
- lines = File.readlines(path).grep_v(/\bprojected_by\s*\(?\s*"/)
652
- bind = /persisted_by\s*\(?\s*(?:"[^"]+"|[a-z_]\w*)\s*\)?/
653
- File.write(path, lines.join.gsub(bind, "persisted_by(\"#{adapter_name}\")"))
654
- end
655
- end
656
- end
657
- end
658
- end