hecks 1.3.0 → 1.5.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 (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -1,6 +1,6 @@
1
1
  module Hecks
2
2
  module Fuzzing
3
- # A value that is DELIBERATELY THE WRONG SHAPE for the attribute it is handed
3
+ # A value that is deliberately the wrong shape for the attribute it is handed
4
4
  # to — the sibling of ValueGenerator, which only ever builds values that satisfy
5
5
  # a declared type.
6
6
  #
@@ -14,7 +14,7 @@ module Hecks
14
14
  # Rendering's whole story), and no generated value
15
15
  # could ever have produced it.
16
16
  #
17
- # These are expected to be REFUSED, and that is the point : a refusal is an
17
+ # These are expected to be refused, and that is the point : a refusal is an
18
18
  # answer, and its wording is pinned byte-for-byte. The bugs live in
19
19
  # the sentence, not in the happy path.
20
20
  module InvalidValueGenerator
@@ -32,15 +32,32 @@ module Hecks
32
32
  null
33
33
  ].freeze
34
34
 
35
+ # Builds one value of the wrong shape for `attribute`, chosen from the
36
+ # confusions that are actually meaningful for its declared type.
37
+ #
38
+ # @param attribute [Bluebook::Attribute] the attribute the corrupted value is
39
+ # handed to
40
+ # @param aggregate [Bluebook::Aggregate] the aggregate that declares
41
+ # `attribute` (and any value objects it references)
42
+ # @param random [Random] the seeded RNG to draw the kind and value from
43
+ # @return [Object] a value of the wrong shape for `attribute` — an Integer,
44
+ # Float, String, Array, Hash, or boolean, depending on the kind drawn
35
45
  def corrupt(attribute, aggregate, random:)
36
46
  kind = kinds_for(attribute, aggregate).sample(random: random)
37
47
  build(kind, attribute, aggregate, random: random)
38
48
  end
39
49
 
40
- # Only the confusions that MEAN anything for this attribute. Offering
50
+ # Only the confusions that mean anything for this attribute. Offering
41
51
  # `scalar_for_object` for a plain Integer would just be a second spelling of
42
52
  # `numeral_string`, and a kind that cannot be wrong for the attribute it is
43
53
  # handed teaches the corpus nothing.
54
+ #
55
+ # @param attribute [Bluebook::Attribute] the attribute to find applicable
56
+ # confusions for
57
+ # @param aggregate [Bluebook::Aggregate] the aggregate that declares
58
+ # `attribute` (and any value objects it references)
59
+ # @return [Array<Symbol>] the subset of `KINDS` that are a real confusion for
60
+ # `attribute`'s declared type
44
61
  def kinds_for(attribute, aggregate)
45
62
  value_object = aggregate.value_object(attribute.type.to_s)
46
63
  return %i[scalar_for_object array_for_scalar null] if value_object
@@ -49,6 +66,16 @@ module Hecks
49
66
  %i[object_for_scalar array_for_scalar boolean_for_string null]
50
67
  end
51
68
 
69
+ # Builds the actual corrupted value for one already-chosen `kind`.
70
+ #
71
+ # @param kind [Symbol] one of `KINDS`
72
+ # @param attribute [Bluebook::Attribute] the attribute the value will be
73
+ # handed to
74
+ # @param aggregate [Bluebook::Aggregate] the aggregate that declares
75
+ # `attribute` (and any value objects it references)
76
+ # @param random [Random] the seeded RNG to draw the value from
77
+ # @return [Object, nil] the corrupted value for `kind`; `nil` if `kind` is not
78
+ # one `KINDS` declares
52
79
  def build(kind, attribute, aggregate, random:)
53
80
  case kind
54
81
  when :object_for_scalar then { "cents" => random.rand(1..1000) }
@@ -60,16 +87,25 @@ module Hecks
60
87
  end
61
88
  end
62
89
 
63
- # A bare scalar where a value object is declared. A SINGLE-FIELD value object
90
+ # A bare scalar where a value object is declared. A single-field value object
64
91
  # legitimately accepts one (that is the standing-in rule every domain relies
65
- # on), so the interesting case is a value object with SEVERAL fields, where a
92
+ # on), so the interesting case is a value object with several fields, where a
66
93
  # scalar cannot stand for anything and the refusal has to say so.
94
+ # @param attribute [Bluebook::Attribute] the value-object-typed attribute to
95
+ # build a bare scalar for
96
+ # @param aggregate [Bluebook::Aggregate] the aggregate that declares
97
+ # `attribute`'s value object
98
+ # @param random [Random] accepted for a uniform call signature with `#build`;
99
+ # this method draws nothing from it
100
+ # @return [String, Hash] `"a bare scalar"` when a scalar would legitimately be
101
+ # accepted (a single-field value object); otherwise a Hash corrupting the
102
+ # sole field's own type instead
67
103
  def scalar_for(attribute, aggregate, random:)
68
104
  value_object = aggregate.value_object(attribute.type.to_s)
69
105
  sole = value_object&.sole_attribute
70
106
  return "a bare scalar" unless sole
71
107
 
72
- # One field, so a scalar is legal — corrupt the FIELD's own type instead,
108
+ # One field, so a scalar is legal — corrupt the field's own type instead,
73
109
  # which is still a shape the attribute cannot accept.
74
110
  { sole.name.to_s => ["nested", "array"] }
75
111
  end
@@ -77,6 +113,9 @@ module Hecks
77
113
  # An attribute the command never declared. `refuse_unknown_arguments` is a
78
114
  # real dispatch step (Vocabulary::AggregateDispatchOrder), and nothing
79
115
  # generated had ever exercised it.
116
+ # @param random [Random] the seeded RNG to draw the name and value from
117
+ # @return [Array(String, String)] a `[name, value]` pair naming an argument
118
+ # no command declares
80
119
  def undeclared_argument(random:)
81
120
  name = %w[colour flavour rank note].sample(random: random)
82
121
  [name, %w[red loud third scribbled].sample(random: random)]
@@ -4,37 +4,41 @@ 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
+ #
9
+ # ## Why rebind at all
8
10
  #
9
11
  # Fuzzing/replay copies a domain to a tmpdir and boots from there
10
12
  # specifically to get zero-history state — `rm_rf`ing the copy's own
11
13
  # `data/` achieves that for a file-based adapter (Memory,
12
- # SqlitePersistence, Heki), because copying the DIRECTORY copies the
14
+ # SqlitePersistence, Heki), because copying the directory copies the
13
15
  # store. It achieves nothing for an adapter that lives outside the
14
16
  # copied directory entirely — Postgres, named by a fixed connection
15
17
  # string in `.world` (examples/pizzas/bluebook/pizzas.world, for
16
18
  # instance). Copying the directory does not copy or isolate the
17
- # DATABASE, so a "fresh" boot against a Postgres-bound domain would
19
+ # database, so a "fresh" boot against a Postgres-bound domain would
18
20
  # still see every record any other run, ever, wrote to it.
19
21
  #
20
22
  # So every `.hecksagon` in the copy gets its persistence binding
21
23
  # rewritten to Memory before booting, and any `projected_by` bind
22
24
  # dropped outright (optional — `Registry#read_repository` already
23
25
  # falls back to the authoritative repository when none exists). The
24
- # domain's own rules and shape are untouched ; only WHICH adapter this
26
+ # domain's own rules and shape are untouched ; only which adapter this
25
27
  # one ephemeral copy answers through changes. What the domain is
26
28
  # bound to for real deployment is never touched — only this tmp copy.
27
29
  #
28
- # `adapter:` (PRD 02) — Memory is the default and the only mode every
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
31
- # same reason PRD 02 exists: 15 declared properties (properties.rb)
32
- # and every fuzz/replay run has only ever been checked against
33
- # Memory's own hand-written repository, never against a real,
34
- # persisted, SQL-backed one — and `spec/adapters/query_agreement_spec
35
- # .rb` already found 4 shipped query bugs from exactly that
36
- # comparison, on a fixed corpus far smaller than what the fuzzer
37
- # generates. `:sqlite` rewrites to `"SqlitePersistence"` — the name
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
38
42
  # `lib/hecks/adapters/driven/sqlite.adapter` actually registers under
39
43
  # (`Sqlite` is the class; `SqlitePersistence` is a thin subclass
40
44
  # that's the one real port binding names) — and needs no `.world`
@@ -44,25 +48,23 @@ module Hecks
44
48
  # `root:`, which `Hecks.boot(copy)` passes as this ephemeral copy's
45
49
  # own directory — a fresh, empty `data/` per run, exactly like
46
50
  # Memory's own zero-history guarantee, just backed by a real SQLite
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
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
59
61
  # (`bin/qa_sweep --persistence-parity`) already has to manage a
60
62
  # disposable database of its own, never a shared one this module could
61
63
  # safely default to.
62
64
  module IsolatedBoot
63
65
  module_function
64
66
 
65
- # PRD 02 (docs/future-features.md) — `adapter:` picks WHICH real
67
+ # PRD 02 (docs/future-features.md) — `adapter:` picks which real
66
68
  # persistence this one ephemeral boot answers through, not just
67
69
  # Memory. `:memory` is the original, zero-config behavior (every
68
70
  # other caller in this codebase that doesn't pass `adapter:` gets
@@ -75,7 +77,7 @@ module Hecks
75
77
  # so `bin/fuzz --adapter postgres` is meant to run with smaller
76
78
  # seed/step counts than the Memory default, not as a like-for-like
77
79
  # swap; see that flag's own comment.
78
- # `database:`/`schema:` are ONLY meaningful for `adapter: :postgres_era`
80
+ # `database:`/`schema:` are only meaningful for `adapter: :postgres_era`
79
81
  # — see `rebind_to_postgres_era!`'s own header for why that mode takes
80
82
  # caller-supplied connection identity instead of a hardcoded shared
81
83
  # constant the way `:postgres` does. Every other adapter ignores both;
@@ -83,6 +85,20 @@ module Hecks
83
85
  # signature per adapter) keeps `SequenceGenerator`/`Replay`'s own
84
86
  # single passthrough (`adapter:`, now joined by these two) uniform
85
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
+ # @yield [String] the freshly rebound copy's root directory, ready to boot
97
+ # @yieldreturn [Object] anything; becomes this method's own return value
98
+ # @return [Object] whatever the given block returns
99
+ # @raise [ArgumentError] if `adapter` is not `:memory`, `:sqlite`, `:postgres`, or
100
+ # `:postgres_era`, or if `adapter: :postgres_era` is given without both
101
+ # `database:` and `schema:`
86
102
  def call(domain_path, adapter: :memory, database: nil, schema: nil)
87
103
  Dir.mktmpdir("hecks-fuzz") do |tmp|
88
104
  copy = File.join(tmp, File.basename(domain_path))
@@ -100,8 +116,8 @@ module Hecks
100
116
  end
101
117
  end
102
118
 
103
- # SYMLINKS ARE FOLLOWED, NOT COPIED. `FileUtils.cp_r` reproduces a
104
- # symlink AS a symlink, and a RELATIVE one then points at nothing
119
+ # Symlinks are followed, not copied. `FileUtils.cp_r` reproduces a
120
+ # symlink as a symlink, and a relative one then points at nothing
105
121
  # from a tmpdir — `lib/hecks/framework/bluebook/compliance
106
122
  # .bluebook` is exactly that, a link to
107
123
  # `examples/compliance/bluebook/compliance.bluebook`, so the whole
@@ -109,10 +125,14 @@ module Hecks
109
125
  # path under /var/folders that had never existed. Nothing about the
110
126
  # domain was wrong; the copy was.
111
127
  #
112
- # `FileUtils.cp` follows a symlink and copies its CONTENT, which is
128
+ # `FileUtils.cp` follows a symlink and copies its content, which is
113
129
  # what an isolated boot wants: the copy has to stand alone, since
114
130
  # rebind! rewrites files in it and must not reach back through a
115
131
  # link into the real tree.
132
+ #
133
+ # @param source [String] the real domain directory to copy
134
+ # @param destination [String] the tmpdir path to copy it into; created if missing
135
+ # @return [void]
116
136
  def copy_dereferencing(source, destination)
117
137
  FileUtils.mkdir_p(destination)
118
138
  Dir.glob(File.join(source, "**", "*"), File::FNM_DOTMATCH).each do |path|
@@ -136,12 +156,17 @@ module Hecks
136
156
  end
137
157
  end
138
158
 
159
+ # Rewrites every `.hecksagon` in the copy to bind through Memory and drops its
160
+ # `.world` files, so the boot needs no settings at all.
161
+ #
162
+ # @param copy [String] the isolated copy's root directory
163
+ # @return [void]
139
164
  def rebind_to_memory!(copy)
140
165
  rewrite_bindings!(copy, "Memory")
141
166
  strip_translations!(copy)
142
167
 
143
- # THE SETTINGS, NOT JUST THE BIND — `WorldBuilder#method_missing`
144
- # stores a settings block under BOTH "verb:adapter" and the bare
168
+ # **The settings, not just the bind** — `WorldBuilder#method_missing`
169
+ # stores a settings block under both "verb:adapter" and the bare
145
170
  # "verb" (world_builder.rb:32-33), so a bind rewritten to Memory
146
171
  # still falls back to whatever adapter's settings were declared
147
172
  # bare — Postgres's `database:`, which Memory does not take and
@@ -154,7 +179,7 @@ module Hecks
154
179
  Dir.glob(File.join(copy, "**", "*.world")).each { |path| File.delete(path) }
155
180
  end
156
181
 
157
- # SAME DANCE AS MEMORY, ONE ADAPTER OVER — `Adapters::Sqlite#
182
+ # Same dance as memory, one adapter over — `Adapters::Sqlite#
158
183
  # resolve_path` (adapters/driven/sqlite.rb) defaults to
159
184
  # `data/<table>.db` under the boot's own root when no `database`
160
185
  # setting is declared, which `data/` already being cleared makes a
@@ -162,30 +187,33 @@ module Hecks
162
187
  # needed" property Memory has. `data/` itself is left for Sqlite to
163
188
  # recreate on first write, same as it always was for Memory/Heki —
164
189
  # nothing here creates it up front.
190
+ #
191
+ # @param copy [String] the isolated copy's root directory
192
+ # @return [void]
165
193
  def rebind_to_sqlite!(copy)
166
194
  rewrite_bindings!(copy, "SqlitePersistence")
167
195
  strip_translations!(copy)
168
196
  Dir.glob(File.join(copy, "**", "*.world")).each { |path| File.delete(path) }
169
197
  end
170
198
 
171
- # THE EXPENSIVE ONE — Postgres has no zero-config default the way
199
+ # **The expensive one** — Postgres has no zero-config default the way
172
200
  # Sqlite/Memory do (`Adapters::Postgres.connect_for` refuses outright
173
201
  # with no `database` setting), so dropping `.world` the way the
174
202
  # other two do would just move the WiringError from "wrong adapter"
175
203
  # to "no adapter." Every domain name this copy declares gets a
176
- # FRESH `.world` written for it instead — not a rewrite of whatever
204
+ # fresh `.world` written for it instead — not a rewrite of whatever
177
205
  # was there, a replacement, same reasoning `rewrite_bindings!`
178
206
  # already applies to `.hecksagon`: this ephemeral boot owns every
179
207
  # binding decision, nothing about a real deployment's own settings
180
208
  # is relevant or safe to half-preserve here.
181
209
  #
182
- # ONE SHARED SCHEMA, DROPPED AND RECREATED BEFORE EVERY BOOT — not a
210
+ # One shared schema, dropped and recreated before every boot — not a
183
211
  # fresh randomly-named one per call. `bin/fuzz` drives every
184
212
  # ephemeral boot sequentially (one `IsolatedBoot.call` fully exits
185
213
  # before the next begins — see that file's own single-threaded
186
214
  # `while` loop), so nothing is ever concurrent here ; a fresh name
187
215
  # every time would just leak schemas in `FUZZ_POSTGRES_DATABASE`
188
- # forever with nothing to ever drop them. If a caller ever DOES
216
+ # forever with nothing to ever drop them. If a caller ever does
189
217
  # start running fuzz adapters concurrently, this needs to move to a
190
218
  # process-unique schema name (`SecureRandom.hex` is already
191
219
  # `require`d here for exactly that day) — flagged, not solved,
@@ -193,17 +221,22 @@ module Hecks
193
221
  FUZZ_POSTGRES_DATABASE = "hecks_fuzz".freeze
194
222
  FUZZ_POSTGRES_SCHEMA = "hecks_fuzz".freeze
195
223
 
224
+ # Rewrites every `.hecksagon` in the copy to bind through Postgres, against the
225
+ # shared `FUZZ_POSTGRES_DATABASE`/`FUZZ_POSTGRES_SCHEMA` scratch schema.
226
+ #
227
+ # @param copy [String] the isolated copy's root directory
228
+ # @return [void]
196
229
  def rebind_to_postgres!(copy)
197
230
  require "pg"
198
231
  rewrite_bindings!(copy, "Postgres")
199
232
  strip_translations!(copy)
200
233
  ensure_fuzz_schema!
201
234
 
202
- # ONE `.world` PER DIRECTORY A `.hecksagon` ACTUALLY LIVES IN, not
235
+ # One `.world` per directory a `.hecksagon` actually lives in, not
203
236
  # one at `copy`'s own root — `Folder#load_domain` resolves a
204
- # SINGLE `bluebook_directory` and globs `*.world` there, non-
237
+ # single `bluebook_directory` and globs `*.world` there, non-
205
238
  # recursively (`Folder#load_each`); a domain can hold several
206
- # `Hecks.hecksagon "<Name>" do ... end` SIBLING blocks in that one
239
+ # `Hecks.hecksagon "<Name>" do ... end` sibling blocks in that one
207
240
  # file (banking.hecksagon declares "Banking", "Governance", and
208
241
  # "Identity" together), so every name found in one `.hecksagon`
209
242
  # file gets bundled into one `.world` written beside it, not
@@ -225,7 +258,7 @@ module Hecks
225
258
  end.join("\n"))
226
259
  end
227
260
 
228
- # Any PRE-EXISTING `.world` this copy shipped with (a real
261
+ # Any pre-existing `.world` this copy shipped with (a real
229
262
  # deployment's own connection string) is now redundant with — and
230
263
  # would conflict with, `Registry#add_world`'s own header on
231
264
  # loading the same domain name twice — the fresh one just
@@ -236,20 +269,22 @@ module Hecks
236
269
  end
237
270
  end
238
271
 
239
- # ADMIN CONNECTION LIVES OUTSIDE THE TMP COPY ENTIRELY — same as
272
+ # Admin connection lives outside the tmp copy entirely — same as
240
273
  # every other real-Postgres spec in this repo (`support/
241
274
  # postgres_probe.rb`'s own header). Database created once per
242
275
  # process and remembered (`@fuzz_database_ready` on this module's
243
276
  # own singleton, the same memoization shape `PostgresProbe
244
277
  # .available?` already uses) ; the schema inside it is dropped and
245
- # recreated on EVERY call, which is what actually isolates one
278
+ # recreated on every call, which is what actually isolates one
246
279
  # ephemeral boot's data from the next.
280
+ #
281
+ # @return [void]
247
282
  def ensure_fuzz_schema!
248
- # `Adapters::Postgres#initialize` opens ONE real `PG::Connection`
249
- # PER AGGREGATE and never explicitly closes it — fine for a
283
+ # `Adapters::Postgres#initialize` opens one real `PG::Connection`
284
+ # per aggregate and never explicitly closes it — fine for a
250
285
  # process that boots once and runs, exactly what every other
251
286
  # caller of this adapter is. A fuzz run boots dozens to hundreds
252
- # of EPHEMERAL times in one process (every seed does at least a
287
+ # of ephemeral times in one process (every seed does at least a
253
288
  # generate + a replay, `replay_is_deterministic` doubles that,
254
289
  # shrinking multiplies it further), and `PG::Connection` only
255
290
  # actually closes its socket when Ruby's GC finalizes the
@@ -261,7 +296,7 @@ module Hecks
261
296
  # ephemeral boots, `PG::ConnectionBad: ... "too many clients
262
297
  # already"`. A `GC.start` here — right before the next ephemeral
263
298
  # boot's connections open, not on some timer — reclaims every
264
- # connection the PREVIOUS boot's now-unreferenced adapters held,
299
+ # connection the previous boot's now-unreferenced adapters held,
265
300
  # keeping the live count bounded regardless of run length. This
266
301
  # is a real constraint on running Postgres in a loop, not
267
302
  # something to route around by connecting less carefully.
@@ -278,7 +313,7 @@ module Hecks
278
313
  end
279
314
 
280
315
  db = PG.connect(dbname: FUZZ_POSTGRES_DATABASE)
281
- # QUIET ON PURPOSE — same as `Adapters::Postgres.connect_for`'s
316
+ # **Quiet on purpose** — same as `Adapters::Postgres.connect_for`'s
282
317
  # own `SET client_min_messages`: a `DROP SCHEMA ... CASCADE` that
283
318
  # actually has something to drop (every boot after the first)
284
319
  # NOTICEs once per dropped object, which is the ordinary case
@@ -292,11 +327,11 @@ module Hecks
292
327
  db.close
293
328
  end
294
329
 
295
- # THE ADAPTER `:postgres` NEVER TOUCHES — `Postgres` and `PostgresEra`
296
- # are SIBLING, NOT interchangeable, adapters (see postgres_era.rb's
330
+ # The adapter `:postgres` never touches — `Postgres` and `PostgresEra`
331
+ # are sibling, not interchangeable, adapters (see postgres_era.rb's
297
332
  # own header: "the only one that declares the LINEAGE capability").
298
333
  # PRD 02 (docs/prds/02-fuzzer-real-adapters.md) shipped `:postgres`
299
- # and explicitly scoped `PostgresEra` OUT: "nothing here touches
334
+ # and explicitly scoped `PostgresEra` out: "nothing here touches
300
335
  # era/lineage machinery." That gap is real, not cosmetic —
301
336
  # `examples/directory` (a `compute`/`rekey` translation edge, the
302
337
  # one domain in this corpus that actually exercises PostgresEra-
@@ -305,23 +340,29 @@ module Hecks
305
340
  # `:memory` included, structurally cannot reach it. This mode closes
306
341
  # that — `bin/qa_sweep --persistence-parity` is its first caller.
307
342
  #
308
- # NO SHARED CONSTANT DATABASE, UNLIKE `:postgres` ABOVE — deliberate.
343
+ # No shared constant database, unlike `:postgres` above — deliberate.
309
344
  # `rebind_to_postgres!`'s own `FUZZ_POSTGRES_DATABASE`/`_SCHEMA` are
310
345
  # module-level constants because `bin/fuzz --adapter postgres` is a
311
346
  # general-purpose, run-it-anytime tool with no caller-tracked
312
347
  # lifecycle of its own. This mode's only caller
313
348
  # (`bin/qa_sweep --persistence-parity`) is different: it dispatches
314
349
  # through `QualityControl::Target.claim!`'s own cross-process lock
315
- # first (see that script's own header), so at most ONE sweep is ever
350
+ # first (see that script's own header), so at most one sweep is ever
316
351
  # 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
352
+ # the caller, not this module, is what owns that database's name and
318
353
  # lifecycle (created, and genuinely dropped, by the caller itself),
319
- # exactly the discipline `spec/qa_sweep_all_spec.rb`'s own header
354
+ # exactly the discipline `spec/support/qa_sweep_all_fixture.rb`'s own header
320
355
  # describes and this repository's persistence-parity work is
321
- # required to follow. Accepting `database:`/`schema:` as REQUIRED
356
+ # required to follow. Accepting `database:`/`schema:` as required
322
357
  # keyword arguments (never a fallback constant) is what keeps that
323
358
  # ownership from silently drifting back onto this module the way
324
359
  # `:postgres`'s own `FUZZ_POSTGRES_DATABASE` already has.
360
+ #
361
+ # @param copy [String] the isolated copy's root directory
362
+ # @param database [String] the caller-owned throwaway database name
363
+ # @param schema [String] the caller-owned throwaway schema name
364
+ # @return [void]
365
+ # @raise [ArgumentError] if `database` or `schema` is empty
325
366
  def rebind_to_postgres_era!(copy, database:, schema:)
326
367
  require "pg"
327
368
  if database.to_s.empty? || schema.to_s.empty?
@@ -334,25 +375,25 @@ module Hecks
334
375
  rewrite_bindings!(copy, "PostgresEra")
335
376
  ensure_postgres_era_schema!(database: database, schema: schema)
336
377
 
337
- # SAME ONE-`.world`-PER-`.hecksagon`-DIRECTORY SHAPE `rebind_to_
378
+ # Same one-`.world`-per-`.hecksagon`-directory shape `rebind_to_
338
379
  # postgres!` already uses, for the identical reason (`Folder#
339
380
  # load_domain` globs `*.world` non-recursively) — see that
340
381
  # 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
382
+ # additionally takes `schema:` (postgres_era.rb's own "shared-
383
+ # instance isolation" comment): the caller-supplied throwaway
343
384
  # schema is what actually isolates this one ephemeral boot from
344
385
  # the next, the same job `FUZZ_POSTGRES_SCHEMA` does for `:postgres`
345
386
  # — `connect_for` itself idempotently `CREATE SCHEMA IF NOT
346
- # EXISTS`s it, so this method only ever needs to DROP it first
387
+ # EXISTS`s it, so this method only ever needs to drop it first
347
388
  # (in `ensure_postgres_era_schema!`, below) for the zero-history
348
389
  # guarantee every other adapter mode already gives.
349
390
  #
350
- # `allow_superuser true` — ON THE RECORD, ON PURPOSE. A bare
391
+ # `allow_superuser true` — on the record, on purpose. A bare
351
392
  # `database` connects as the ambient Postgres user, and
352
393
  # PostgresEra refuses to boot at all when that user is a
353
394
  # superuser (its era write-fence is row-level security, which a
354
395
  # superuser walks through — `Lineage#check_fence_applies!`,
355
- # BUG#24). That refusal protects a REAL ledger from an old
396
+ # BUG#24). That refusal protects a real ledger from an old
356
397
  # checkout's stale writes; nothing here is one. This is an
357
398
  # ephemeral boot into a throwaway schema the caller itself
358
399
  # creates and drops, whose data no second checkout ever shares,
@@ -386,20 +427,25 @@ module Hecks
386
427
  end
387
428
  end
388
429
 
389
- # THE ZERO-HISTORY GUARANTEE FOR THIS MODE — `DROP SCHEMA ... CASCADE`
430
+ # The zero-history guarantee for this mode — `DROP SCHEMA ... CASCADE`
390
431
  # before every ephemeral boot, mirroring `ensure_fuzz_schema!` above
391
432
  # (same `GC.start`-before-connecting fix for the identical
392
433
  # `max_connections` exhaustion that method's own comment documents —
393
434
  # `PostgresEra` opens real `PG::Connection`s exactly like `Postgres`
394
- # does, same unclosed-until-GC'd lifetime). The DATABASE itself is
435
+ # does, same unclosed-until-GC'd lifetime). The database itself is
395
436
  # created here too, idempotently (`CREATE DATABASE IF NOT EXISTS`
396
437
  # has no Postgres spelling, hence the existence check) — but never
397
- # DROPPED here: this module creates it once per process because
438
+ # dropped here: this module creates it once per process because
398
439
  # `Hecks.boot` needs it to exist before `PostgresEra.connect_for`'s
399
440
  # 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
441
+ # again is the caller's own job (its name and lifecycle belong to
401
442
  # the caller — see `rebind_to_postgres_era!`'s own header), not
402
443
  # something this per-ephemeral-boot helper should ever do mid-sweep.
444
+ #
445
+ # @param database [String] the caller-owned database name, created if it does not
446
+ # already exist
447
+ # @param schema [String] the caller-owned schema name, dropped and recreated
448
+ # @return [void]
403
449
  def ensure_postgres_era_schema!(database:, schema:)
404
450
  GC.start
405
451
 
@@ -417,7 +463,7 @@ module Hecks
417
463
  db.close
418
464
  end
419
465
 
420
- # THE SHARED REWRITE — factored out of `rebind_to_memory!` when
466
+ # The shared rewrite — factored out of `rebind_to_memory!` when
421
467
  # Sqlite/Postgres modes needed the identical `.hecksagon` surgery
422
468
  # with only the target adapter name differing. `persisted_by`/
423
469
  # `projected_by` can be spelled two ways: aggregate-scoped
@@ -431,46 +477,57 @@ module Hecks
431
477
  # #read_repository` already falls back to the authoritative
432
478
  # repository when none exists, so a read model this ephemeral copy
433
479
  # 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
480
+ # A `compute`/`rekey` translation edge refuses to boot at all under
481
+ # any non-lineage-capable adapter — found live, wiring this very
436
482
  # mode up against `examples/directory`: `Runtime::EraCheck
437
- # .check_compute_rules!` (era_check.rb) runs UNCONDITIONALLY for
483
+ # .check_compute_rules!` (era_check.rb) runs unconditionally for
438
484
  # every loaded bluebook once the era plugin is loaded at all
439
485
  # (`bin/qa_sweep`'s own top-of-file `require "hecks/ports/
440
486
  # persistence/plugins/era"`, needed for the ledger's own
441
487
  # 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
488
+ # rules require the Postgres adapter" — for any aggregate whose
489
+ # lineage carries a `compute` rule and whose bound adapter is not
444
490
  # lineage-capable. `PostgresEra` is the only adapter that answers
445
491
  # `lineage_capable? == true` (postgres_era.rb's own `self.
446
- # lineage_capable? = true`) — plain `Postgres` does NOT, so this
492
+ # lineage_capable? = true`) — plain `Postgres` does not, so this
447
493
  # refusal was already real for `:postgres`/`:sqlite`/`:memory`
448
494
  # alike, for any domain with a translation edge, before this
449
495
  # mode's own `:postgres_era` ever existed. This is very likely the
450
- # MECHANICAL reason `examples/directory` had to be shelved out of
496
+ # mechanical reason `examples/directory` had to be shelved out of
451
497
  # `hecks_qa`'s own rotation in the first place — not merely "less
452
- # interesting to fuzz on Memory," but "cannot BOOT on Memory at
498
+ # interesting to fuzz on Memory," but "cannot boot on Memory at
453
499
  # all" once the era plugin is loaded, which every real
454
500
  # `bin/qa_sweep` invocation already does.
455
501
  #
456
- # THE FIX IS TO DROP THE EDGE, NOT TO CHASE THE REFUSAL — an
502
+ # The fix is to drop the edge, not to chase the refusal — an
457
503
  # ephemeral, zero-history replay boot (every mode `IsolatedBoot`
458
504
  # 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
505
+ # first place, so the translation edge is irrelevant to anything a
460
506
  # fuzz/replay run actually exercises (ordinary command dispatch
461
- # against a fresh boot) — it only ever matters at MINT time,
507
+ # against a fresh boot) — it only ever matters at mint time,
462
508
  # against a real, pre-existing database
463
509
  # (`PostgresEra::LineageManager.check!`, a wholly separate,
464
510
  # human-approved path this harness was never meant to reach).
465
511
  # Dropping it here is exactly the same move `rebind_to_memory!`
466
512
  # already makes for `.world` (irrelevant/conflicting settings for
467
513
  # an ephemeral boot, deleted outright) — never called for
468
- # `:postgres_era` itself, where the bound adapter genuinely IS
514
+ # `:postgres_era` itself, where the bound adapter genuinely is
469
515
  # lineage-capable and the edge causes no refusal to begin with.
516
+ #
517
+ # @param copy [String] the isolated copy's root directory
518
+ # @return [void]
470
519
  def strip_translations!(copy)
471
520
  Dir.glob(File.join(copy, "**", "translations", "*.bluebook")).each { |path| File.delete(path) }
472
521
  end
473
522
 
523
+ # Rewrites every `persisted_by` bind — aggregate-scoped or bare at the
524
+ # hecksagon's own root — in the copy's `.hecksagon` files to name `adapter_name`,
525
+ # and drops every `projected_by` bind outright.
526
+ #
527
+ # @param copy [String] the isolated copy's root directory
528
+ # @param adapter_name [String] the port binding name to rewrite every
529
+ # `persisted_by` to, such as `"Memory"` or `"SqlitePersistence"`
530
+ # @return [void]
474
531
  def rewrite_bindings!(copy, adapter_name)
475
532
  Dir.glob(File.join(copy, "**", "*.hecksagon")).each do |path|
476
533
  lines = File.readlines(path).grep_v(/\bprojected_by\s*\(?\s*"/)