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
@@ -0,0 +1,79 @@
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
+ # Lists the nondeterministic field names declared for one comparison group.
52
+ #
53
+ # @param group [Symbol] a key of `FIELDS`, such as `:query_row` or `:event`
54
+ # @return [Array<Symbol>] the field names declared for `group`
55
+ # @raise [KeyError] if `group` names no declared group
56
+ def names(group)
57
+ FIELDS.fetch(group).keys
58
+ end
59
+
60
+ # Every declared name, across every group.
61
+ #
62
+ # @return [Array<Symbol>] the union of every group's field names, deduplicated
63
+ def all_names
64
+ FIELDS.values.flat_map(&:keys).uniq
65
+ end
66
+
67
+ # `hash` without `group`'s fields — symbol keys, the shape every
68
+ # Ruby-side history carries.
69
+ #
70
+ # @param hash [Hash] a symbol-keyed row, event, or history hash to compare
71
+ # @param group [Symbol] a key of `FIELDS` naming which fields to drop
72
+ # @return [Hash] `hash` with `group`'s declared fields removed
73
+ # @raise [KeyError] if `group` names no declared group
74
+ def strip(hash, group)
75
+ hash.except(*names(group))
76
+ end
77
+ end
78
+ end
79
+ end
@@ -1,16 +1,19 @@
1
1
  require "json"
2
2
  require_relative "replay"
3
+ require_relative "nondeterministic"
3
4
 
4
5
  module Hecks
5
6
  module Fuzzing
6
- # THE SECOND DIFFERENTIAL AXIS — bin/qa_sweep's own `diff_ruby_vs_rust`
7
- # compares two DIFFERENT ENGINES (the Ruby interpreter vs the compiled
8
- # Rust kernel) against the SAME persistence (Memory, always — see
9
- # `SequenceGenerator`'s own header: sequence GENERATION stays
7
+ # ## The second differential axis
8
+ #
9
+ # bin/qa_sweep's own `diff_ruby_vs_rust`
10
+ # compares two different engines (the Ruby interpreter vs the compiled
11
+ # Rust kernel) against the same persistence (Memory, always — see
12
+ # `SequenceGenerator`'s own header: sequence generation stays
10
13
  # Memory-only, and `IsolatedBoot`'s own header explains why every
11
14
  # existing fuzz/replay path structurally cannot reach a real Postgres-
12
15
  # bound domain's own SQL compilation). This module compares the
13
- # opposite pairing: the SAME ONE Ruby engine, against two DIFFERENT
16
+ # opposite pairing: the same one Ruby engine, against two different
14
17
  # persistences — Memory (the reference, exactly as fast and as
15
18
  # deterministic as every other fuzz check) and a real, disposable
16
19
  # `PostgresEra` database (the actual SQL compute/rekey/query-pushdown
@@ -26,20 +29,24 @@ module Hecks
26
29
  # Memory, unconditionally, no matter what `directory.world` itself
27
30
  # declares.
28
31
  #
29
- # NOT a replacement for `bin/qa_sweep`'s own Ruby-vs-Rust differential
32
+ # ## A separate axis, not a replacement
33
+ #
34
+ # Not a replacement for `bin/qa_sweep`'s own Ruby-vs-Rust differential
30
35
  # mode — a genuinely separate axis, opt-in (`--persistence-parity`),
31
36
  # because this one pays for a real `PG.connect` and real SQL per
32
37
  # dispatch where Memory-vs-Rust pays for neither. See `bin/qa_sweep`'s
33
38
  # own `--persistence-parity` handling for the seed-count dial that
34
39
  # keeps that cost bounded.
35
40
  #
36
- # GENERALIZED TO `left:`/`right:` — originally hardcoded to Memory vs
41
+ # ## `left:`/`right:`
42
+ #
43
+ # Generalized — originally hardcoded to Memory vs
37
44
  # PostgresEra (the only pairing that existed), now any two of
38
45
  # `IsolatedBoot`'s own adapter symbols (`:memory`, `:sqlite`,
39
46
  # `:postgres`, `:postgres_era`). Defaults preserve the original
40
47
  # pairing exactly, so every existing caller (this file's own spec,
41
48
  # `bin/qa_sweep`'s `--persistence-parity`) is unchanged. The second
42
- # pairing this generalization exists FOR is Memory vs SQLite
49
+ # pairing this generalization exists for is Memory vs SQLite
43
50
  # (`QualityControlDials::ADAPTER_PARITY_PAIRS`, `bin/qa_sweep`'s own
44
51
  # `adapter_parity_sqlite` mode) — `:sqlite` is nearly as cheap as
45
52
  # Memory itself (`IsolatedBoot#rebind_to_sqlite!`'s own header: an
@@ -48,7 +55,9 @@ module Hecks
48
55
  # loop instead of needing a deferred wave of its own the way
49
56
  # PostgresEra does.
50
57
  #
51
- # `database:`/`schema:` — REQUIRED only when `:postgres_era` is one of
58
+ # ## `database:`/`schema:`
59
+ #
60
+ # Required only when `:postgres_era` is one of
52
61
  # the two adapters (the caller — today, only `bin/qa_sweep` — owns the
53
62
  # disposable database's whole lifecycle: created before the sweep,
54
63
  # dropped after — see that script's own comment, and the discipline
@@ -58,21 +67,21 @@ module Hecks
58
67
  module PersistenceParity
59
68
  module_function
60
69
 
61
- # THE SAME SIX FIELDS `bin/qa_sweep`'s own `diff_ruby_vs_rust`
70
+ # The same six fields `bin/qa_sweep`'s own `diff_ruby_vs_rust`
62
71
  # compares (its own comment: "instances, events, refusals, queries,
63
72
  # sagas, reactions") — deliberately the identical set, so a report
64
73
  # this mode produces reads exactly like the sibling mode's own,
65
74
  # differing only in which two things were compared, not in what
66
75
  # "found something" means.
67
76
  #
68
- # SIMPLER NORMALIZATION THAN `diff_ruby_vs_rust`, on purpose — that
69
- # method reduces Rust's OWN JSON-over-stdout output to "wire
77
+ # Simpler normalization than `diff_ruby_vs_rust`, on purpose — that
78
+ # method reduces Rust's own JSON-over-stdout output to "wire
70
79
  # precision" and filters known Ruby/Rust structural gaps, because
71
80
  # it is comparing two genuinely different engines that are allowed
72
81
  # to differ in already-catalogued, understood ways. Both sides here
73
- # are the SAME Ruby engine (`Replay.call`, called twice, adapter
82
+ # are the same Ruby engine (`Replay.call`, called twice, adapter
74
83
  # only) — there is no second engine's own known-gap catalogue to
75
- # filter against, so any real difference IS the finding. Both
84
+ # filter against, so any real difference is the finding. Both
76
85
  # results still round-trip through `JSON.generate`/`JSON.parse`
77
86
  # before comparing, matching `diff_ruby_vs_rust`'s own discipline —
78
87
  # not because either side needs a wire-format reduction, but so
@@ -80,6 +89,21 @@ module Hecks
80
89
  # sides identically (a `Runtime::Value`, a `Symbol` key, a `Time`
81
90
  # nobody asked for — none of that survives an accidental leak into
82
91
  # this comparison unnoticed).
92
+ #
93
+ # @param domain_path [String] path to the domain directory to boot
94
+ # @param steps [Array<Hash>] the step list to replay against both adapters
95
+ # @param left [Symbol] the first adapter to replay against, one of
96
+ # `IsolatedBoot`'s adapter symbols (`:memory`, `:sqlite`, `:postgres`,
97
+ # `:postgres_era`)
98
+ # @param right [Symbol] the second adapter to replay against, same set as `left`
99
+ # @param database [String, nil] connection identity for `:postgres_era`; required
100
+ # when `left` or `right` is `:postgres_era`, ignored otherwise
101
+ # @param schema [String, nil] disposable schema name for `:postgres_era`; required
102
+ # when `left` or `right` is `:postgres_era`, ignored otherwise
103
+ # @return [Array<Hash>] divergence entries, each `{field: String, left => Object,
104
+ # right => Object}` — `left`'s and `right`'s own adapter symbols become the
105
+ # entry's own keys, holding each side's JSON-shaped value for that field; empty
106
+ # if both sides agree on every field
83
107
  def diff(domain_path, steps, left: :memory, right: :postgres_era, database: nil, schema: nil)
84
108
  left_result = Replay.call(domain_path, steps, adapter: left, database: database, schema: schema)
85
109
  right_result = Replay.call(domain_path, steps, adapter: right, database: database, schema: schema)
@@ -94,8 +118,25 @@ module Hecks
94
118
  divergences
95
119
  end
96
120
 
121
+ # Round-trips `value` through JSON, the same wire-precision reduction
122
+ # `diff_ruby_vs_rust` uses so `Hash#==`/`Array#==` compares plain,
123
+ # JSON-shaped data on both sides.
124
+ #
125
+ # @param value [Object] any JSON-serializable value from a replay result
126
+ # @return [Object] `value`, JSON-round-tripped: Symbol keys become Strings,
127
+ # and any non-JSON-native value surfaces as its own JSON form
97
128
  def as_json(value) = JSON.parse(JSON.generate(value))
98
129
 
130
+ # Compares both sides' stored instances.
131
+ #
132
+ # @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
133
+ # @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
134
+ # @param left [Symbol] the adapter `left_result` was replayed against; becomes
135
+ # this entry's own key
136
+ # @param right [Symbol] the adapter `right_result` was replayed against; becomes
137
+ # this entry's own key
138
+ # @return [Array<Hash>] one `{field: "instances", left => Object, right => Object}`
139
+ # entry if the two sides' JSON-shaped instances differ; empty otherwise
99
140
  def diff_instances(left_result, right_result, left, right)
100
141
  l = as_json(left_result[:instances])
101
142
  r = as_json(right_result[:instances])
@@ -104,6 +145,16 @@ module Hecks
104
145
  [{ field: "instances", left => l, right => r }]
105
146
  end
106
147
 
148
+ # Compares both sides' emitted events.
149
+ #
150
+ # @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
151
+ # @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
152
+ # @param left [Symbol] the adapter `left_result` was replayed against; becomes
153
+ # this entry's own key
154
+ # @param right [Symbol] the adapter `right_result` was replayed against; becomes
155
+ # this entry's own key
156
+ # @return [Array<Hash>] one `{field: "events", left => Object, right => Object}`
157
+ # entry if the two sides' JSON-shaped events differ; empty otherwise
107
158
  def diff_events(left_result, right_result, left, right)
108
159
  l = as_json(left_result[:events])
109
160
  r = as_json(right_result[:events])
@@ -112,11 +163,22 @@ module Hecks
112
163
  [{ field: "events", left => l, right => r }]
113
164
  end
114
165
 
166
+ # Compares both sides' refusals.
167
+ #
115
168
  # `verb:`/`kind:` normalized to plain strings the same way
116
169
  # `diff_ruby_vs_rust`'s own `ruby_refusals` mapping does — both
117
170
  # sides here already answer strings (`Replay#refusal_kind` always
118
171
  # returns one), so this is belt-and-suspenders consistency with the
119
172
  # sibling mode's own shape, not a real coercion.
173
+ #
174
+ # @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
175
+ # @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
176
+ # @param left [Symbol] the adapter `left_result` was replayed against; becomes
177
+ # this entry's own key
178
+ # @param right [Symbol] the adapter `right_result` was replayed against; becomes
179
+ # this entry's own key
180
+ # @return [Array<Hash>] one `{field: "refusals", left => Object, right => Object}`
181
+ # entry if the two sides' normalized refusals differ; empty otherwise
120
182
  def diff_refusals(left_result, right_result, left, right)
121
183
  normalize = lambda do |refusals|
122
184
  refusals.map { |r| { "verb" => r[:verb].to_s, "kind" => r[:kind].to_s, "error" => r[:error] } }
@@ -128,14 +190,22 @@ module Hecks
128
190
  [{ field: "refusals", left => l, right => r }]
129
191
  end
130
192
 
131
- # `instances_at:` dropped from every entry — the same reason
132
- # `diff_ruby_vs_rust` excludes it (`row.except(:instances_at)`):
133
- # it is a full state snapshot taken for the QUERY oracle's own use,
134
- # already covered by `diff_instances` above, and would make every
135
- # query-step entry re-litigate the SAME instances divergence a
136
- # second time under a different field name.
193
+ # Compares both sides' query answers.
194
+ #
195
+ # `Nondeterministic`'s `query_row` group dropped from every entry —
196
+ # the same group `Differential.diff` drops, for the reason declared
197
+ # there (already covered by `diff_instances` above).
198
+ #
199
+ # @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
200
+ # @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
201
+ # @param left [Symbol] the adapter `left_result` was replayed against; becomes
202
+ # this entry's own key
203
+ # @param right [Symbol] the adapter `right_result` was replayed against; becomes
204
+ # this entry's own key
205
+ # @return [Array<Hash>] one `{field: "queries", left => Object, right => Object}`
206
+ # entry if the two sides' stripped, JSON-shaped queries differ; empty otherwise
137
207
  def diff_queries(left_result, right_result, left, right)
138
- strip = ->(rows) { rows.map { |row| row.except(:instances_at) } }
208
+ strip = ->(rows) { rows.map { |row| Nondeterministic.strip(row, :query_row) } }
139
209
  l = as_json(strip.call(left_result[:queries]))
140
210
  r = as_json(strip.call(right_result[:queries]))
141
211
  return [] if l == r
@@ -143,6 +213,16 @@ module Hecks
143
213
  [{ field: "queries", left => l, right => r }]
144
214
  end
145
215
 
216
+ # Compares both sides' saga logs.
217
+ #
218
+ # @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
219
+ # @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
220
+ # @param left [Symbol] the adapter `left_result` was replayed against; becomes
221
+ # this entry's own key
222
+ # @param right [Symbol] the adapter `right_result` was replayed against; becomes
223
+ # this entry's own key
224
+ # @return [Array<Hash>] one `{field: "sagas", left => Object, right => Object}`
225
+ # entry if the two sides' JSON-shaped sagas differ; empty otherwise
146
226
  def diff_sagas(left_result, right_result, left, right)
147
227
  l = as_json(left_result[:sagas])
148
228
  r = as_json(right_result[:sagas])
@@ -151,6 +231,16 @@ module Hecks
151
231
  [{ field: "sagas", left => l, right => r }]
152
232
  end
153
233
 
234
+ # Compares both sides' reaction logs.
235
+ #
236
+ # @param left_result [Hash] `left`'s replay result, as returned by `Fuzzing::Replay.call`
237
+ # @param right_result [Hash] `right`'s replay result, as returned by `Fuzzing::Replay.call`
238
+ # @param left [Symbol] the adapter `left_result` was replayed against; becomes
239
+ # this entry's own key
240
+ # @param right [Symbol] the adapter `right_result` was replayed against; becomes
241
+ # this entry's own key
242
+ # @return [Array<Hash>] one `{field: "reactions", left => Object, right => Object}`
243
+ # entry if the two sides' JSON-shaped reactions differ; empty otherwise
154
244
  def diff_reactions(left_result, right_result, left, right)
155
245
  l = as_json(left_result[:reactions])
156
246
  r = as_json(right_result[:reactions])
@@ -1,7 +1,7 @@
1
1
  module Hecks
2
2
  module Fuzzing
3
3
  module Properties
4
- # ANGLE-9 — `corrects` (retroactive correction) had exactly one
4
+ # Angle-9 — `corrects` (retroactive correction) had exactly one
5
5
  # declaration anywhere in the corpus (`examples/banking/bluebook/
6
6
  # deposit_accounts.bluebook:353`, aggregate-level) and no property
7
7
  # anywhere in this file ever checked it, and no `FEATURE_COVERAGE`
@@ -9,7 +9,7 @@ module Hecks
9
9
  # stress_domains/corrections` gives it its first real coverage; see
10
10
  # that domain's own NOTES.md for what it found (an entity-level
11
11
  # `corrects` crashes Ruby at dispatch outright, and Rust's own
12
- # generated code has NO admissibility check for it AT ALL — neither
12
+ # generated code has no admissibility check for it at all — neither
13
13
  # engine can be compared on the untested combination this property
14
14
  # was written to watch, which is itself the headline finding).
15
15
  module Corrections
@@ -19,32 +19,37 @@ module Hecks
19
19
  # entities`, below, is the same recursive walk `SequenceGenerator::
20
20
  # Catalog#each_entity_chain` already uses, for the identical reason:
21
21
  # `Aggregate#entities`/`Entity#entities` nest, ADR 0026, S17). For
22
- # every such command, every event THIS history actually recorded
22
+ # every such command, every event this history actually recorded
23
23
  # under one of the command's own `emits` names must have an event
24
24
  # named by the command's own `corrects` target — same aggregate-
25
- # qualified name, same id — appearing STRICTLY EARLIER in the same
25
+ # qualified name, same id — appearing strictly earlier in the same
26
26
  # history.
27
27
  #
28
28
  # `history[:events]` is already in occurrence order (`Replay.call`'s
29
29
  # own `runtime.events`, appended as each step dispatches) — "earlier"
30
30
  # is therefore "earlier in this array," no timestamp comparison
31
31
  # needed, and no per-step attribution back to which command produced
32
- # which event is needed either: an event's own declared NAME already
32
+ # which event is needed either: an event's own declared name already
33
33
  # identifies the one command in its aggregate that can produce it
34
34
  # (`AggregateBuilder::Sealing#seal_correction_targets`'s own
35
35
  # `emitted_by` hash reads the identical fact, one level shallower).
36
36
  #
37
- # WHY THIS CANNOT BE `GUARANTEED_BY_CONSTRUCTION` THE WAY THE
38
- # AGGREGATE-LEVEL CASE ALMOST IS: `CommandRules::Admissibility#
37
+ # Why this cannot be `GUARANTEED_BY_CONSTRUCTION` the way the
38
+ # aggregate-level case almost is: `CommandRules::Admissibility#
39
39
  # enforce_correction_target` (the dispatch-time check) and
40
40
  # `AggregateBuilder::Sealing#seal_correction_targets` (the build-time
41
- # check) both exist ONLY for an aggregate-level `corrects` —
41
+ # check) both exist only for an aggregate-level `corrects` —
42
42
  # `EntityInterpreter#step_enforce_givens` never calls the former at
43
43
  # all, and the latter walks only `@commands` (the aggregate's own
44
44
  # top-level list), never `@entities`. An entity-level `corrects`
45
- # mutation is invisible to BOTH doors today — this property is the
45
+ # mutation is invisible to both doors today — this property is the
46
46
  # only thing anywhere, on either engine, that would ever catch one
47
47
  # going wrong.
48
+ #
49
+ # @param history [Hash] a replayed history as returned by `Replay.call`
50
+ # @return [true, String] true if every event emitted by a `corrects`-bearing
51
+ # command has a matching, strictly earlier corrected event in the same
52
+ # history; otherwise a message listing every unmatched correction
48
53
  def corrections_reference_an_emitted_event(history)
49
54
  violations = []
50
55
 
@@ -72,6 +77,17 @@ module Hecks
72
77
  violations.empty? || violations.uniq.join("; ")
73
78
  end
74
79
 
80
+ # Finds every occurrence of `produced_event_name`, on `aggregate_key`, with no
81
+ # matching `corrected_event` for the same id appearing earlier in `events`.
82
+ #
83
+ # @param events [Array<Hash>] `history[:events]`, in occurrence order
84
+ # @param aggregate_key [String] `"domain::AggregateName"` the events belong to
85
+ # @param produced_event_name [String] name of the event a `corrects` mutation's
86
+ # command emits
87
+ # @param corrected_event [String] name of the event the mutation claims to correct
88
+ # @param command_name [String] the command's own `hecks_name`, for the message
89
+ # @return [Array<String>] one message per occurrence with no matching earlier
90
+ # corrected event; empty when every occurrence is matched
75
91
  def unmatched_corrections(events, aggregate_key, produced_event_name, corrected_event, command_name)
76
92
  own_events = events.each_with_index.select do |event, _index|
77
93
  event[:name] == produced_event_name && event[:aggregate] == aggregate_key
@@ -90,6 +106,15 @@ module Hecks
90
106
  end
91
107
  end
92
108
 
109
+ # Yields every command declared on `owner`, then recurses into each of its
110
+ # entities to yield theirs too, at any nesting depth (ADR 0026, S17).
111
+ #
112
+ # @param owner [Bluebook::Aggregate, Bluebook::Entity] the aggregate or entity
113
+ # whose own commands, and whose entities' commands, to walk
114
+ # @yield [command] once per declared command, aggregate-level or nested
115
+ # @yieldparam command [Bluebook::Command] a command declared on `owner` or one
116
+ # of its entities
117
+ # @return [void]
93
118
  def each_command_including_entities(owner, &block)
94
119
  owner.commands.each(&block)
95
120
  owner.entities.each { |entity| each_command_including_entities(entity, &block) }