hecks 1.2.0 → 1.4.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 (358) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
@@ -4,30 +4,30 @@ require "securerandom"
4
4
 
5
5
  module Hecks
6
6
  module Fuzzing
7
- # A FRESH, IN-PROCESS ADAPTER FOR EVERY EPHEMERAL BOOT.
7
+ # A fresh, in-process adapter for every ephemeral boot.
8
8
  #
9
9
  # Fuzzing/replay copies a domain to a tmpdir and boots from there
10
10
  # specifically to get zero-history state — `rm_rf`ing the copy's own
11
11
  # `data/` achieves that for a file-based adapter (Memory,
12
- # SqlitePersistence, Heki), because copying the DIRECTORY copies the
12
+ # SqlitePersistence, Heki), because copying the directory copies the
13
13
  # store. It achieves nothing for an adapter that lives outside the
14
14
  # copied directory entirely — Postgres, named by a fixed connection
15
15
  # string in `.world` (examples/pizzas/bluebook/pizzas.world, for
16
16
  # instance). Copying the directory does not copy or isolate the
17
- # DATABASE, so a "fresh" boot against a Postgres-bound domain would
17
+ # database, so a "fresh" boot against a Postgres-bound domain would
18
18
  # still see every record any other run, ever, wrote to it.
19
19
  #
20
20
  # So every `.hecksagon` in the copy gets its persistence binding
21
21
  # rewritten to Memory before booting, and any `projected_by` bind
22
22
  # dropped outright (optional — `Registry#read_repository` already
23
23
  # falls back to the authoritative repository when none exists). The
24
- # domain's own rules and shape are untouched ; only WHICH adapter this
24
+ # domain's own rules and shape are untouched ; only which adapter this
25
25
  # one ephemeral copy answers through changes. What the domain is
26
26
  # bound to for real deployment is never touched — only this tmp copy.
27
27
  #
28
28
  # `adapter:` (PRD 02) — Memory is the default and the only mode every
29
29
  # existing caller still gets with no change. `:sqlite` rebinds to the
30
- # REAL SQLite adapter instead of the in-memory one, for exactly the
30
+ # real SQLite adapter instead of the in-memory one, for exactly the
31
31
  # same reason PRD 02 exists: 15 declared properties (properties.rb)
32
32
  # and every fuzz/replay run has only ever been checked against
33
33
  # Memory's own hand-written repository, never against a real,
@@ -44,15 +44,25 @@ module Hecks
44
44
  # `root:`, which `Hecks.boot(copy)` passes as this ephemeral copy's
45
45
  # own directory — a fresh, empty `data/` per run, exactly like
46
46
  # Memory's own zero-history guarantee, just backed by a real SQLite
47
- # file instead of a Hash. Postgres/PostgresEra are deliberately NOT
48
- # added here: a real Postgres run needs a live server and shared
49
- # connection settings this in-process, no-adapter-config path has no
50
- # place to source safely — that stays `io: true`-gated, direct-adapter
51
- # coverage (`spec/adapters/driven/postgres_*_spec.rb`), not this.
47
+ # file instead of a Hash. Stale as of this paragraph's original
48
+ # writing both `Postgres` (PRD 02, docs/prds/02-fuzzer-real-
49
+ # adapters.md) and `PostgresEra` (this mode's own header, below,
50
+ # `rebind_to_postgres_era!`) since gained real `adapter:` modes here.
51
+ # Each writes its own fresh `.world` per `.hecksagon` rather than
52
+ # relying on the zero-config default Sqlite/Memory get, and each
53
+ # needs a real, reachable Postgres server — but "no place to source a
54
+ # connection safely" turned out not to be true: `:postgres` sources
55
+ # one shared, permanent scratch database/schema this module itself
56
+ # owns (see `FUZZ_POSTGRES_DATABASE`'s own header); `:postgres_era`
57
+ # instead requires the caller to supply (and own the lifecycle of) its
58
+ # own throwaway `database:`/`schema:`, since its only caller
59
+ # (`bin/qa_sweep --persistence-parity`) already has to manage a
60
+ # disposable database of its own, never a shared one this module could
61
+ # safely default to.
52
62
  module IsolatedBoot
53
63
  module_function
54
64
 
55
- # PRD 02 (docs/future-features.md) — `adapter:` picks WHICH real
65
+ # PRD 02 (docs/future-features.md) — `adapter:` picks which real
56
66
  # persistence this one ephemeral boot answers through, not just
57
67
  # Memory. `:memory` is the original, zero-config behavior (every
58
68
  # other caller in this codebase that doesn't pass `adapter:` gets
@@ -65,23 +75,33 @@ module Hecks
65
75
  # so `bin/fuzz --adapter postgres` is meant to run with smaller
66
76
  # seed/step counts than the Memory default, not as a like-for-like
67
77
  # swap; see that flag's own comment.
68
- def call(domain_path, adapter: :memory)
78
+ # `database:`/`schema:` are only meaningful for `adapter: :postgres_era`
79
+ # — see `rebind_to_postgres_era!`'s own header for why that mode takes
80
+ # caller-supplied connection identity instead of a hardcoded shared
81
+ # constant the way `:postgres` does. Every other adapter ignores both;
82
+ # accepting them unconditionally here (rather than a separate method
83
+ # signature per adapter) keeps `SequenceGenerator`/`Replay`'s own
84
+ # single passthrough (`adapter:`, now joined by these two) uniform
85
+ # across all four modes.
86
+ def call(domain_path, adapter: :memory, database: nil, schema: nil)
69
87
  Dir.mktmpdir("hecks-fuzz") do |tmp|
70
88
  copy = File.join(tmp, File.basename(domain_path))
71
89
  copy_dereferencing(domain_path, copy)
72
90
  FileUtils.rm_rf(File.join(copy, "data"))
73
91
  case adapter
74
- when :memory then rebind_to_memory!(copy)
75
- when :sqlite then rebind_to_sqlite!(copy)
76
- when :postgres then rebind_to_postgres!(copy)
77
- else raise ArgumentError, "unknown fuzz adapter #{adapter.inspect} — :memory, :sqlite, or :postgres"
92
+ when :memory then rebind_to_memory!(copy)
93
+ when :sqlite then rebind_to_sqlite!(copy)
94
+ when :postgres then rebind_to_postgres!(copy)
95
+ when :postgres_era then rebind_to_postgres_era!(copy, database: database, schema: schema)
96
+ else raise ArgumentError,
97
+ "unknown fuzz adapter #{adapter.inspect} — :memory, :sqlite, :postgres, or :postgres_era"
78
98
  end
79
99
  yield copy
80
100
  end
81
101
  end
82
102
 
83
- # SYMLINKS ARE FOLLOWED, NOT COPIED. `FileUtils.cp_r` reproduces a
84
- # symlink AS a symlink, and a RELATIVE one then points at nothing
103
+ # Symlinks are followed, not copied. `FileUtils.cp_r` reproduces a
104
+ # symlink as a symlink, and a relative one then points at nothing
85
105
  # from a tmpdir — `lib/hecks/framework/bluebook/compliance
86
106
  # .bluebook` is exactly that, a link to
87
107
  # `examples/compliance/bluebook/compliance.bluebook`, so the whole
@@ -89,7 +109,7 @@ module Hecks
89
109
  # path under /var/folders that had never existed. Nothing about the
90
110
  # domain was wrong; the copy was.
91
111
  #
92
- # `FileUtils.cp` follows a symlink and copies its CONTENT, which is
112
+ # `FileUtils.cp` follows a symlink and copies its content, which is
93
113
  # what an isolated boot wants: the copy has to stand alone, since
94
114
  # rebind! rewrites files in it and must not reach back through a
95
115
  # link into the real tree.
@@ -118,9 +138,10 @@ module Hecks
118
138
 
119
139
  def rebind_to_memory!(copy)
120
140
  rewrite_bindings!(copy, "Memory")
141
+ strip_translations!(copy)
121
142
 
122
- # THE SETTINGS, NOT JUST THE BIND — `WorldBuilder#method_missing`
123
- # stores a settings block under BOTH "verb:adapter" and the bare
143
+ # **The settings, not just the bind** — `WorldBuilder#method_missing`
144
+ # stores a settings block under both "verb:adapter" and the bare
124
145
  # "verb" (world_builder.rb:32-33), so a bind rewritten to Memory
125
146
  # still falls back to whatever adapter's settings were declared
126
147
  # bare — Postgres's `database:`, which Memory does not take and
@@ -133,7 +154,7 @@ module Hecks
133
154
  Dir.glob(File.join(copy, "**", "*.world")).each { |path| File.delete(path) }
134
155
  end
135
156
 
136
- # SAME DANCE AS MEMORY, ONE ADAPTER OVER — `Adapters::Sqlite#
157
+ # Same dance as memory, one adapter over — `Adapters::Sqlite#
137
158
  # resolve_path` (adapters/driven/sqlite.rb) defaults to
138
159
  # `data/<table>.db` under the boot's own root when no `database`
139
160
  # setting is declared, which `data/` already being cleared makes a
@@ -143,27 +164,28 @@ module Hecks
143
164
  # nothing here creates it up front.
144
165
  def rebind_to_sqlite!(copy)
145
166
  rewrite_bindings!(copy, "SqlitePersistence")
167
+ strip_translations!(copy)
146
168
  Dir.glob(File.join(copy, "**", "*.world")).each { |path| File.delete(path) }
147
169
  end
148
170
 
149
- # THE EXPENSIVE ONE — Postgres has no zero-config default the way
171
+ # **The expensive one** — Postgres has no zero-config default the way
150
172
  # Sqlite/Memory do (`Adapters::Postgres.connect_for` refuses outright
151
173
  # with no `database` setting), so dropping `.world` the way the
152
174
  # other two do would just move the WiringError from "wrong adapter"
153
175
  # to "no adapter." Every domain name this copy declares gets a
154
- # FRESH `.world` written for it instead — not a rewrite of whatever
176
+ # fresh `.world` written for it instead — not a rewrite of whatever
155
177
  # was there, a replacement, same reasoning `rewrite_bindings!`
156
178
  # already applies to `.hecksagon`: this ephemeral boot owns every
157
179
  # binding decision, nothing about a real deployment's own settings
158
180
  # is relevant or safe to half-preserve here.
159
181
  #
160
- # ONE SHARED SCHEMA, DROPPED AND RECREATED BEFORE EVERY BOOT — not a
182
+ # One shared schema, dropped and recreated before every boot — not a
161
183
  # fresh randomly-named one per call. `bin/fuzz` drives every
162
184
  # ephemeral boot sequentially (one `IsolatedBoot.call` fully exits
163
185
  # before the next begins — see that file's own single-threaded
164
186
  # `while` loop), so nothing is ever concurrent here ; a fresh name
165
187
  # every time would just leak schemas in `FUZZ_POSTGRES_DATABASE`
166
- # forever with nothing to ever drop them. If a caller ever DOES
188
+ # forever with nothing to ever drop them. If a caller ever does
167
189
  # start running fuzz adapters concurrently, this needs to move to a
168
190
  # process-unique schema name (`SecureRandom.hex` is already
169
191
  # `require`d here for exactly that day) — flagged, not solved,
@@ -174,13 +196,14 @@ module Hecks
174
196
  def rebind_to_postgres!(copy)
175
197
  require "pg"
176
198
  rewrite_bindings!(copy, "Postgres")
199
+ strip_translations!(copy)
177
200
  ensure_fuzz_schema!
178
201
 
179
- # ONE `.world` PER DIRECTORY A `.hecksagon` ACTUALLY LIVES IN, not
202
+ # One `.world` per directory a `.hecksagon` actually lives in, not
180
203
  # one at `copy`'s own root — `Folder#load_domain` resolves a
181
- # SINGLE `bluebook_directory` and globs `*.world` there, non-
204
+ # single `bluebook_directory` and globs `*.world` there, non-
182
205
  # recursively (`Folder#load_each`); a domain can hold several
183
- # `Hecks.hecksagon "<Name>" do ... end` SIBLING blocks in that one
206
+ # `Hecks.hecksagon "<Name>" do ... end` sibling blocks in that one
184
207
  # file (banking.hecksagon declares "Banking", "Governance", and
185
208
  # "Identity" together), so every name found in one `.hecksagon`
186
209
  # file gets bundled into one `.world` written beside it, not
@@ -202,7 +225,7 @@ module Hecks
202
225
  end.join("\n"))
203
226
  end
204
227
 
205
- # Any PRE-EXISTING `.world` this copy shipped with (a real
228
+ # Any pre-existing `.world` this copy shipped with (a real
206
229
  # deployment's own connection string) is now redundant with — and
207
230
  # would conflict with, `Registry#add_world`'s own header on
208
231
  # loading the same domain name twice — the fresh one just
@@ -213,20 +236,20 @@ module Hecks
213
236
  end
214
237
  end
215
238
 
216
- # ADMIN CONNECTION LIVES OUTSIDE THE TMP COPY ENTIRELY — same as
239
+ # Admin connection lives outside the tmp copy entirely — same as
217
240
  # every other real-Postgres spec in this repo (`support/
218
241
  # postgres_probe.rb`'s own header). Database created once per
219
242
  # process and remembered (`@fuzz_database_ready` on this module's
220
243
  # own singleton, the same memoization shape `PostgresProbe
221
244
  # .available?` already uses) ; the schema inside it is dropped and
222
- # recreated on EVERY call, which is what actually isolates one
245
+ # recreated on every call, which is what actually isolates one
223
246
  # ephemeral boot's data from the next.
224
247
  def ensure_fuzz_schema!
225
- # `Adapters::Postgres#initialize` opens ONE real `PG::Connection`
226
- # PER AGGREGATE and never explicitly closes it — fine for a
248
+ # `Adapters::Postgres#initialize` opens one real `PG::Connection`
249
+ # per aggregate and never explicitly closes it — fine for a
227
250
  # process that boots once and runs, exactly what every other
228
251
  # caller of this adapter is. A fuzz run boots dozens to hundreds
229
- # of EPHEMERAL times in one process (every seed does at least a
252
+ # of ephemeral times in one process (every seed does at least a
230
253
  # generate + a replay, `replay_is_deterministic` doubles that,
231
254
  # shrinking multiplies it further), and `PG::Connection` only
232
255
  # actually closes its socket when Ruby's GC finalizes the
@@ -238,7 +261,7 @@ module Hecks
238
261
  # ephemeral boots, `PG::ConnectionBad: ... "too many clients
239
262
  # already"`. A `GC.start` here — right before the next ephemeral
240
263
  # boot's connections open, not on some timer — reclaims every
241
- # connection the PREVIOUS boot's now-unreferenced adapters held,
264
+ # connection the previous boot's now-unreferenced adapters held,
242
265
  # keeping the live count bounded regardless of run length. This
243
266
  # is a real constraint on running Postgres in a loop, not
244
267
  # something to route around by connecting less carefully.
@@ -255,7 +278,7 @@ module Hecks
255
278
  end
256
279
 
257
280
  db = PG.connect(dbname: FUZZ_POSTGRES_DATABASE)
258
- # QUIET ON PURPOSE — same as `Adapters::Postgres.connect_for`'s
281
+ # **Quiet on purpose** — same as `Adapters::Postgres.connect_for`'s
259
282
  # own `SET client_min_messages`: a `DROP SCHEMA ... CASCADE` that
260
283
  # actually has something to drop (every boot after the first)
261
284
  # NOTICEs once per dropped object, which is the ordinary case
@@ -269,7 +292,132 @@ module Hecks
269
292
  db.close
270
293
  end
271
294
 
272
- # THE SHARED REWRITEfactored out of `rebind_to_memory!` when
295
+ # The adapter `:postgres` never touches `Postgres` and `PostgresEra`
296
+ # are sibling, not interchangeable, adapters (see postgres_era.rb's
297
+ # own header: "the only one that declares the LINEAGE capability").
298
+ # PRD 02 (docs/prds/02-fuzzer-real-adapters.md) shipped `:postgres`
299
+ # and explicitly scoped `PostgresEra` out: "nothing here touches
300
+ # era/lineage machinery." That gap is real, not cosmetic —
301
+ # `examples/directory` (a `compute`/`rekey` translation edge, the
302
+ # one domain in this corpus that actually exercises PostgresEra-
303
+ # bound SQL compilation) had to be shelved out of `hecks_qa`'s own
304
+ # rotation for exactly this reason: every existing fuzz/replay path,
305
+ # `:memory` included, structurally cannot reach it. This mode closes
306
+ # that — `bin/qa_sweep --persistence-parity` is its first caller.
307
+ #
308
+ # No shared constant database, unlike `:postgres` above — deliberate.
309
+ # `rebind_to_postgres!`'s own `FUZZ_POSTGRES_DATABASE`/`_SCHEMA` are
310
+ # module-level constants because `bin/fuzz --adapter postgres` is a
311
+ # general-purpose, run-it-anytime tool with no caller-tracked
312
+ # lifecycle of its own. This mode's only caller
313
+ # (`bin/qa_sweep --persistence-parity`) is different: it dispatches
314
+ # through `QualityControl::Target.claim!`'s own cross-process lock
315
+ # first (see that script's own header), so at most one sweep is ever
316
+ # touching a given target's own disposable database at a time — but
317
+ # the caller, not this module, is what owns that database's name and
318
+ # lifecycle (created, and genuinely dropped, by the caller itself),
319
+ # exactly the discipline `spec/support/qa_sweep_all_fixture.rb`'s own header
320
+ # describes and this repository's persistence-parity work is
321
+ # required to follow. Accepting `database:`/`schema:` as required
322
+ # keyword arguments (never a fallback constant) is what keeps that
323
+ # ownership from silently drifting back onto this module the way
324
+ # `:postgres`'s own `FUZZ_POSTGRES_DATABASE` already has.
325
+ def rebind_to_postgres_era!(copy, database:, schema:)
326
+ require "pg"
327
+ if database.to_s.empty? || schema.to_s.empty?
328
+ raise ArgumentError,
329
+ "adapter: :postgres_era requires both database: and schema: — a throwaway database/schema " \
330
+ "THIS CALLER creates and drops itself (see rebind_to_postgres_era!'s own header). " \
331
+ "There is no shared default, unlike :postgres, so the caller cannot forget to own the lifecycle."
332
+ end
333
+
334
+ rewrite_bindings!(copy, "PostgresEra")
335
+ ensure_postgres_era_schema!(database: database, schema: schema)
336
+
337
+ # Same one-`.world`-per-`.hecksagon`-directory shape `rebind_to_
338
+ # postgres!` already uses, for the identical reason (`Folder#
339
+ # load_domain` globs `*.world` non-recursively) — see that
340
+ # method's own comment on `world_path` above. `PostgresEra`
341
+ # additionally takes `schema:` (postgres_era.rb's own "shared-
342
+ # instance isolation" comment): the caller-supplied throwaway
343
+ # schema is what actually isolates this one ephemeral boot from
344
+ # the next, the same job `FUZZ_POSTGRES_SCHEMA` does for `:postgres`
345
+ # — `connect_for` itself idempotently `CREATE SCHEMA IF NOT
346
+ # EXISTS`s it, so this method only ever needs to drop it first
347
+ # (in `ensure_postgres_era_schema!`, below) for the zero-history
348
+ # guarantee every other adapter mode already gives.
349
+ #
350
+ # `allow_superuser true` — on the record, on purpose. A bare
351
+ # `database` connects as the ambient Postgres user, and
352
+ # PostgresEra refuses to boot at all when that user is a
353
+ # superuser (its era write-fence is row-level security, which a
354
+ # superuser walks through — `Lineage#check_fence_applies!`,
355
+ # BUG#24). That refusal protects a real ledger from an old
356
+ # checkout's stale writes; nothing here is one. This is an
357
+ # ephemeral boot into a throwaway schema the caller itself
358
+ # creates and drops, whose data no second checkout ever shares,
359
+ # and what it compares is Memory's answers against PostgresEra's
360
+ # own SQL — the era fence is not under test and cannot be
361
+ # crossed. So opt in explicitly rather than make every
362
+ # persistence-parity run first provision a fenced role for a
363
+ # database it is about to throw away; the one-line warning
364
+ # PostgresEra prints per boot under the opt-in is the honest
365
+ # price. Inert on a machine whose ambient user is ordinary.
366
+ Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |hecksagon_path|
367
+ names = File.read(hecksagon_path).scan(/Hecks\.hecksagon\s+"([^"]+)"/).flatten.uniq
368
+ next if names.empty?
369
+
370
+ world_path = File.join(File.dirname(hecksagon_path), "hecks_fuzz_postgres_era.world")
371
+ File.write(world_path, names.map do |name|
372
+ <<~WORLD
373
+ Hecks.world "#{name}" do
374
+ persisted_by("PostgresEra") do
375
+ database "#{database}"
376
+ schema "#{schema}"
377
+ allow_superuser true
378
+ end
379
+ end
380
+ WORLD
381
+ end.join("\n"))
382
+ end
383
+
384
+ Dir.glob(File.join(copy, "**", "*.world")).each do |path|
385
+ File.delete(path) unless File.basename(path) == "hecks_fuzz_postgres_era.world"
386
+ end
387
+ end
388
+
389
+ # The zero-history guarantee for this mode — `DROP SCHEMA ... CASCADE`
390
+ # before every ephemeral boot, mirroring `ensure_fuzz_schema!` above
391
+ # (same `GC.start`-before-connecting fix for the identical
392
+ # `max_connections` exhaustion that method's own comment documents —
393
+ # `PostgresEra` opens real `PG::Connection`s exactly like `Postgres`
394
+ # does, same unclosed-until-GC'd lifetime). The database itself is
395
+ # created here too, idempotently (`CREATE DATABASE IF NOT EXISTS`
396
+ # has no Postgres spelling, hence the existence check) — but never
397
+ # dropped here: this module creates it once per process because
398
+ # `Hecks.boot` needs it to exist before `PostgresEra.connect_for`'s
399
+ # own `PG.connect(dbname: ...)` can succeed at all, but dropping it
400
+ # again is the caller's own job (its name and lifecycle belong to
401
+ # the caller — see `rebind_to_postgres_era!`'s own header), not
402
+ # something this per-ephemeral-boot helper should ever do mid-sweep.
403
+ def ensure_postgres_era_schema!(database:, schema:)
404
+ GC.start
405
+
406
+ admin = PG.connect(dbname: "postgres")
407
+ exists = admin.exec_params(
408
+ "SELECT 1 FROM pg_database WHERE datname = $1", [database]
409
+ ).ntuples.positive?
410
+ admin.exec(%(CREATE DATABASE "#{database}")) unless exists
411
+ admin.close
412
+
413
+ db = PG.connect(dbname: database)
414
+ db.exec("SET client_min_messages = warning")
415
+ quoted = db.quote_ident(schema)
416
+ db.exec("DROP SCHEMA IF EXISTS #{quoted} CASCADE")
417
+ db.close
418
+ end
419
+
420
+ # The shared rewrite — factored out of `rebind_to_memory!` when
273
421
  # Sqlite/Postgres modes needed the identical `.hecksagon` surgery
274
422
  # with only the target adapter name differing. `persisted_by`/
275
423
  # `projected_by` can be spelled two ways: aggregate-scoped
@@ -283,6 +431,46 @@ module Hecks
283
431
  # #read_repository` already falls back to the authoritative
284
432
  # repository when none exists, so a read model this ephemeral copy
285
433
  # never wires is simply unread, not broken.
434
+ # A `compute`/`rekey` translation edge refuses to boot at all under
435
+ # any non-lineage-capable adapter — found live, wiring this very
436
+ # mode up against `examples/directory`: `Runtime::EraCheck
437
+ # .check_compute_rules!` (era_check.rb) runs unconditionally for
438
+ # every loaded bluebook once the era plugin is loaded at all
439
+ # (`bin/qa_sweep`'s own top-of-file `require "hecks/ports/
440
+ # persistence/plugins/era"`, needed for the ledger's own
441
+ # PostgresEra-bound aggregates), and refuses outright — "compute
442
+ # rules require the Postgres adapter" — for any aggregate whose
443
+ # lineage carries a `compute` rule and whose bound adapter is not
444
+ # lineage-capable. `PostgresEra` is the only adapter that answers
445
+ # `lineage_capable? == true` (postgres_era.rb's own `self.
446
+ # lineage_capable? = true`) — plain `Postgres` does not, so this
447
+ # refusal was already real for `:postgres`/`:sqlite`/`:memory`
448
+ # alike, for any domain with a translation edge, before this
449
+ # mode's own `:postgres_era` ever existed. This is very likely the
450
+ # mechanical reason `examples/directory` had to be shelved out of
451
+ # `hecks_qa`'s own rotation in the first place — not merely "less
452
+ # interesting to fuzz on Memory," but "cannot boot on Memory at
453
+ # all" once the era plugin is loaded, which every real
454
+ # `bin/qa_sweep` invocation already does.
455
+ #
456
+ # The fix is to drop the edge, not to chase the refusal — an
457
+ # ephemeral, zero-history replay boot (every mode `IsolatedBoot`
458
+ # offers) never has a pre-existing era-1 row to translate in the
459
+ # first place, so the translation edge is irrelevant to anything a
460
+ # fuzz/replay run actually exercises (ordinary command dispatch
461
+ # against a fresh boot) — it only ever matters at mint time,
462
+ # against a real, pre-existing database
463
+ # (`PostgresEra::LineageManager.check!`, a wholly separate,
464
+ # human-approved path this harness was never meant to reach).
465
+ # Dropping it here is exactly the same move `rebind_to_memory!`
466
+ # already makes for `.world` (irrelevant/conflicting settings for
467
+ # an ephemeral boot, deleted outright) — never called for
468
+ # `:postgres_era` itself, where the bound adapter genuinely is
469
+ # lineage-capable and the edge causes no refusal to begin with.
470
+ def strip_translations!(copy)
471
+ Dir.glob(File.join(copy, "**", "translations", "*.bluebook")).each { |path| File.delete(path) }
472
+ end
473
+
286
474
  def rewrite_bindings!(copy, adapter_name)
287
475
  Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |path|
288
476
  lines = File.readlines(path).grep_v(/\bprojected_by\s*\(?\s*"/)
@@ -0,0 +1,67 @@
1
+ module Hecks
2
+ module Fuzzing
3
+ # The one declared set of FIELDS that leave a comparison — partition,
4
+ # not filter. Every comparison in lib/hecks/fuzzing that drops a field
5
+ # before comparing two histories names a group here instead of writing
6
+ # its own literal `except(...)` list, so the reason for dropping a field
7
+ # is written exactly once, beside the field.
8
+ #
9
+ # Two specs hold this set honest (spec/fuzzing/nondeterministic_spec.rb):
10
+ # a literal except-list naming any of these fields anywhere under
11
+ # lib/hecks/fuzzing fails the build (the tolerance must be declared
12
+ # here, not re-derived at a call site), and every declared field must
13
+ # actually be produced where its group says — a field nothing emits any
14
+ # more is a stale tolerance and fails too.
15
+ #
16
+ # Groups are keyed by the shape the field rides on, since the same name
17
+ # can be compared on one surface and dropped on another:
18
+ #
19
+ # - `query_row` — one entry of `Replay.call`'s `:queries`.
20
+ # - `outbox_row` — one row of an `:outbox_traces` entry's `:rows`.
21
+ # - `event` — an event hash in full `Event#to_h` form (an outbox
22
+ # row's `:event`, or a Rust binary's `events` entries,
23
+ # string-keyed there).
24
+ # - `history` — the top-level `Replay.call` history hash itself.
25
+ module Nondeterministic
26
+ FIELDS = {
27
+ query_row: {
28
+ instances_at: "a full state snapshot taken for the query oracle's own use " \
29
+ "(Properties::Querying); the same state is already compared as `instances`, " \
30
+ "so keeping it would re-report one instances divergence under every query step"
31
+ }.freeze,
32
+ outbox_row: {
33
+ event_uid: "`Runtime::Outbox::Fanout#rows_for`'s own SecureRandom.uuid, minted fresh per " \
34
+ "enqueue and kept off `Event#to_h` — relay bookkeeping, never something the " \
35
+ "domain produced, so two otherwise-identical replays carry two different uuids",
36
+ delivery_id: "`\"\#{event_uid}/\#{consumer}\"` — inherits event_uid's per-enqueue uuid"
37
+ }.freeze,
38
+ event: {
39
+ occurred_at: "a wall-clock read; never reproducible byte-for-byte between two independent " \
40
+ "runs (Replay's own projected `:events` leave it off for the same reason)"
41
+ }.freeze,
42
+ history: {
43
+ bluebook: "a live IR object (the first-loaded bluebook) handed to properties — an object " \
44
+ "identity, not a value, so two boots never compare equal on it",
45
+ bluebooks: "the full live IR map, keyed by domain — object identities, same as `bluebook`"
46
+ }.freeze
47
+ }.freeze
48
+
49
+ module_function
50
+
51
+ def names(group)
52
+ FIELDS.fetch(group).keys
53
+ end
54
+
55
+ # Every declared name, across every group.
56
+ def all_names
57
+ FIELDS.values.flat_map(&:keys).uniq
58
+ end
59
+
60
+ # `hash` without `group`'s fields — symbol keys, the shape every
61
+ # Ruby-side history carries.
62
+ def strip(hash, group)
63
+ hash.except(*names(group))
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,161 @@
1
+ require "json"
2
+ require_relative "replay"
3
+ require_relative "nondeterministic"
4
+
5
+ module Hecks
6
+ module Fuzzing
7
+ # **The second differential axis** — bin/qa_sweep's own `diff_ruby_vs_rust`
8
+ # compares two different engines (the Ruby interpreter vs the compiled
9
+ # Rust kernel) against the same persistence (Memory, always — see
10
+ # `SequenceGenerator`'s own header: sequence generation stays
11
+ # Memory-only, and `IsolatedBoot`'s own header explains why every
12
+ # existing fuzz/replay path structurally cannot reach a real Postgres-
13
+ # bound domain's own SQL compilation). This module compares the
14
+ # opposite pairing: the same one Ruby engine, against two different
15
+ # persistences — Memory (the reference, exactly as fast and as
16
+ # deterministic as every other fuzz check) and a real, disposable
17
+ # `PostgresEra` database (the actual SQL compute/rekey/query-pushdown
18
+ # path a Postgres-bound domain's users really hit).
19
+ #
20
+ # `examples/directory` is why this exists at all: the one domain in
21
+ # this corpus with a real `compute`/`rekey` translation edge
22
+ # (docs/prds/02-fuzzer-real-adapters.md's own "What shipped" section
23
+ # names it directly), which no existing fuzz/replay/property check has
24
+ # ever been able to exercise against real PostgresEra SQL — every one
25
+ # of them boots through `IsolatedBoot`, and until `adapter:
26
+ # :postgres_era` existed (isolated_boot.rb's own header), that meant
27
+ # Memory, unconditionally, no matter what `directory.world` itself
28
+ # declares.
29
+ #
30
+ # Not a replacement for `bin/qa_sweep`'s own Ruby-vs-Rust differential
31
+ # mode — a genuinely separate axis, opt-in (`--persistence-parity`),
32
+ # because this one pays for a real `PG.connect` and real SQL per
33
+ # dispatch where Memory-vs-Rust pays for neither. See `bin/qa_sweep`'s
34
+ # own `--persistence-parity` handling for the seed-count dial that
35
+ # keeps that cost bounded.
36
+ #
37
+ # Generalized to `left:`/`right:` — originally hardcoded to Memory vs
38
+ # PostgresEra (the only pairing that existed), now any two of
39
+ # `IsolatedBoot`'s own adapter symbols (`:memory`, `:sqlite`,
40
+ # `:postgres`, `:postgres_era`). Defaults preserve the original
41
+ # pairing exactly, so every existing caller (this file's own spec,
42
+ # `bin/qa_sweep`'s `--persistence-parity`) is unchanged. The second
43
+ # pairing this generalization exists for is Memory vs SQLite
44
+ # (`QualityControlDials::ADAPTER_PARITY_PAIRS`, `bin/qa_sweep`'s own
45
+ # `adapter_parity_sqlite` mode) — `:sqlite` is nearly as cheap as
46
+ # Memory itself (`IsolatedBoot#rebind_to_sqlite!`'s own header: an
47
+ # on-disk file, no server, no disposable database/schema lifecycle to
48
+ # own), so that pairing folds straight into the ordinary per-seed
49
+ # loop instead of needing a deferred wave of its own the way
50
+ # PostgresEra does.
51
+ #
52
+ # `database:`/`schema:` — required only when `:postgres_era` is one of
53
+ # the two adapters (the caller — today, only `bin/qa_sweep` — owns the
54
+ # disposable database's whole lifecycle: created before the sweep,
55
+ # dropped after — see that script's own comment, and the discipline
56
+ # `IsolatedBoot#rebind_to_postgres_era!` itself refuses to let a
57
+ # caller skip); every other adapter ignores both, same as `Replay.
58
+ # call`/`IsolatedBoot.call` already do for their own `adapter:`.
59
+ module PersistenceParity
60
+ module_function
61
+
62
+ # The same six fields `bin/qa_sweep`'s own `diff_ruby_vs_rust`
63
+ # compares (its own comment: "instances, events, refusals, queries,
64
+ # sagas, reactions") — deliberately the identical set, so a report
65
+ # this mode produces reads exactly like the sibling mode's own,
66
+ # differing only in which two things were compared, not in what
67
+ # "found something" means.
68
+ #
69
+ # Simpler normalization than `diff_ruby_vs_rust`, on purpose — that
70
+ # method reduces Rust's own JSON-over-stdout output to "wire
71
+ # precision" and filters known Ruby/Rust structural gaps, because
72
+ # it is comparing two genuinely different engines that are allowed
73
+ # to differ in already-catalogued, understood ways. Both sides here
74
+ # are the same Ruby engine (`Replay.call`, called twice, adapter
75
+ # only) — there is no second engine's own known-gap catalogue to
76
+ # filter against, so any real difference is the finding. Both
77
+ # results still round-trip through `JSON.generate`/`JSON.parse`
78
+ # before comparing, matching `diff_ruby_vs_rust`'s own discipline —
79
+ # not because either side needs a wire-format reduction, but so
80
+ # `Hash#==`/`Array#==` compares plain, JSON-shaped data on both
81
+ # sides identically (a `Runtime::Value`, a `Symbol` key, a `Time`
82
+ # nobody asked for — none of that survives an accidental leak into
83
+ # this comparison unnoticed).
84
+ def diff(domain_path, steps, left: :memory, right: :postgres_era, database: nil, schema: nil)
85
+ left_result = Replay.call(domain_path, steps, adapter: left, database: database, schema: schema)
86
+ right_result = Replay.call(domain_path, steps, adapter: right, database: database, schema: schema)
87
+
88
+ divergences = []
89
+ divergences.concat(diff_instances(left_result, right_result, left, right))
90
+ divergences.concat(diff_events(left_result, right_result, left, right))
91
+ divergences.concat(diff_refusals(left_result, right_result, left, right))
92
+ divergences.concat(diff_queries(left_result, right_result, left, right))
93
+ divergences.concat(diff_sagas(left_result, right_result, left, right))
94
+ divergences.concat(diff_reactions(left_result, right_result, left, right))
95
+ divergences
96
+ end
97
+
98
+ def as_json(value) = JSON.parse(JSON.generate(value))
99
+
100
+ def diff_instances(left_result, right_result, left, right)
101
+ l = as_json(left_result[:instances])
102
+ r = as_json(right_result[:instances])
103
+ return [] if l == r
104
+
105
+ [{ field: "instances", left => l, right => r }]
106
+ end
107
+
108
+ def diff_events(left_result, right_result, left, right)
109
+ l = as_json(left_result[:events])
110
+ r = as_json(right_result[:events])
111
+ return [] if l == r
112
+
113
+ [{ field: "events", left => l, right => r }]
114
+ end
115
+
116
+ # `verb:`/`kind:` normalized to plain strings the same way
117
+ # `diff_ruby_vs_rust`'s own `ruby_refusals` mapping does — both
118
+ # sides here already answer strings (`Replay#refusal_kind` always
119
+ # returns one), so this is belt-and-suspenders consistency with the
120
+ # sibling mode's own shape, not a real coercion.
121
+ def diff_refusals(left_result, right_result, left, right)
122
+ normalize = lambda do |refusals|
123
+ refusals.map { |r| { "verb" => r[:verb].to_s, "kind" => r[:kind].to_s, "error" => r[:error] } }
124
+ end
125
+ l = normalize.call(left_result[:refusals])
126
+ r = normalize.call(right_result[:refusals])
127
+ return [] if l == r
128
+
129
+ [{ field: "refusals", left => l, right => r }]
130
+ end
131
+
132
+ # `Nondeterministic`'s `query_row` group dropped from every entry —
133
+ # the same group `Differential.diff` drops, for the reason declared
134
+ # there (already covered by `diff_instances` above).
135
+ def diff_queries(left_result, right_result, left, right)
136
+ strip = ->(rows) { rows.map { |row| Nondeterministic.strip(row, :query_row) } }
137
+ l = as_json(strip.call(left_result[:queries]))
138
+ r = as_json(strip.call(right_result[:queries]))
139
+ return [] if l == r
140
+
141
+ [{ field: "queries", left => l, right => r }]
142
+ end
143
+
144
+ def diff_sagas(left_result, right_result, left, right)
145
+ l = as_json(left_result[:sagas])
146
+ r = as_json(right_result[:sagas])
147
+ return [] if l == r
148
+
149
+ [{ field: "sagas", left => l, right => r }]
150
+ end
151
+
152
+ def diff_reactions(left_result, right_result, left, right)
153
+ l = as_json(left_result[:reactions])
154
+ r = as_json(right_result[:reactions])
155
+ return [] if l == r
156
+
157
+ [{ field: "reactions", left => l, right => r }]
158
+ end
159
+ end
160
+ end
161
+ end