hecks 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
@@ -12,18 +12,18 @@ module Hecks
12
12
  # verb-to-declaration lookups (#command_for_verb,
13
13
  # #effective_guard_descriptions) each resolves a refusal against.
14
14
  module Guards
15
- # `Query#options`' OTHER HALF — TenantScope.apply's own contract
15
+ # `Query#options`' other half — TenantScope.apply's own contract
16
16
  # (tenant_scope.rb), independently restated as a property rather
17
17
  # than exercised only through whatever the generator happens to
18
- # try. NOT closed by the generator here on purpose: SafeDepositBox.
18
+ # try. Not closed by the generator here on purpose: SafeDepositBox.
19
19
  # Rented — the only real corpus query declaring `authorize` at
20
- # all — declares ZERO attributes of its own, so StepBuilder#args_for
20
+ # all — declares zero attributes of its own, so StepBuilder#args_for
21
21
  # always hands it `{}` and TenantScope.apply refuses every
22
22
  # generated attempt, unconditionally (confirmed: no successful ask
23
23
  # against an authorize-bearing query reaches this property via the
24
24
  # standard battery today). Extending the generator to invent a
25
25
  # `tenant:` value ran into a separate, real finding along the way —
26
- # SafeDepositBox is COMPOSITE-identified (`identified_by` is nil
26
+ # SafeDepositBox is composite-identified (`identified_by` is nil
27
27
  # for it — Runtime::Identified#derive_identity), so the generator's
28
28
  # existing `known_ids` pool (keyed by `aggregate.identified_by ||
29
29
  # "id"`) tracks a stray, never-real scalar for it rather than its
@@ -37,10 +37,10 @@ module Hecks
37
37
  # where a `tenant:` arg came from.
38
38
  #
39
39
  # Two claims, matching TenantScope.apply's own two branches: every
40
- # SUCCESSFUL answer's own tenant field agrees with the tenant arg
40
+ # successful answer's own tenant field agrees with the tenant arg
41
41
  # given (the WhereClause TenantScope injects is a Symbol reference
42
- # into args, resolved dynamically — this checks the OUTCOME, not
43
- # re-deriving that resolution) ; every ask MISSING a required
42
+ # into args, resolved dynamically — this checks the outcome, not
43
+ # re-deriving that resolution) ; every ask missing a required
44
44
  # tenant: refuses with the declared wording, never succeeds. A
45
45
  # refusal for an unrelated reason with the tenant arg present is
46
46
  # not this property's claim either way — skipped, not graded.
@@ -92,14 +92,14 @@ module Hecks
92
92
  offenders.empty? || offenders.join("; ")
93
93
  end
94
94
 
95
- # EVERY GIVEN/ENSURES REFUSAL A RUN ACTUALLY RAISED NAMES A RULE
96
- # THE COMMAND ACTUALLY DECLARES. `GivenNotMet`/`EnsuresNotMet` both
95
+ # Every given/ensures refusal a run actually raised names a rule
96
+ # the command actually declares. `GivenNotMet`/`EnsuresNotMet` both
97
97
  # quote their guard's own `description` verbatim
98
98
  # (command_rules/admissibility.rb: `"#{command.hecks_name} refused
99
- # — #{given.description}"`) — the SAME text `behavior.bluebook`'s
99
+ # — #{given.description}"`) — the same text `behavior.bluebook`'s
100
100
  # own `Rule`/Command.Ensure hold as `Rule#description`, so a
101
101
  # refusal whose quoted text is not among the refusing command's
102
- # OWN `guard_descriptions` (Behaviour::Command, both givens and
102
+ # own `guard_descriptions` (Behaviour::Command, both givens and
103
103
  # ensures) is either a stale message surviving a renamed rule, a
104
104
  # rule firing against the wrong command's own guard set, or the
105
105
  # wording drifting out from under the declaration it is supposed
@@ -107,7 +107,7 @@ module Hecks
107
107
  # guards, some through a cross-aggregate dereference) are exactly
108
108
  # the surface this exists to hold to its word.
109
109
  #
110
- # `kind:` is what tells a guard refusal apart from the FOUR other
110
+ # `kind:` is what tells a guard refusal apart from the four other
111
111
  # `RefusalWording` templates sharing the identical "X refused — Y"
112
112
  # shape (LifecycleRefused/transition_blocked, both TypeMismatch
113
113
  # object-reference templates, Unauthorized/role_mismatch) — see
@@ -138,16 +138,119 @@ module Hecks
138
138
  offenders.empty? || offenders.join("; ")
139
139
  end
140
140
 
141
- # A DECLARED PROCESS MANAGER'S OWN COMMAND `command.hecks_name`,
141
+ # Angle-8's own write-side half. `authorize_scopes_or_refuses`
142
+ # (above) enforces `TenantScope.apply`'s boundary, and that
143
+ # boundary exists only for queries/read models — `authorize
144
+ # policy, tenant: field` is a word `QuerySpecification::Common::
145
+ # DSL#authorize_impl` grants to `QueryBuilder`/`ReadModelBuilder`
146
+ # alone; `CommandBuilder` never includes that module, so no
147
+ # bluebook can declare it on a command at all (confirmed by
148
+ # reading the grammar directly, not inferred). A write that
149
+ # carries a `reference_to` from one tenant-scoped record into
150
+ # another's is checked by nothing at dispatch time: `TenantScope`
151
+ # never runs for a command, and no runtime `given`/`ensures`
152
+ # anywhere in this corpus reads a cross-aggregate tenant field
153
+ # either. `qa/stress_domains/tenant_ledger` exists to give this
154
+ # property a real place to fire.
155
+ #
156
+ # The rule: an aggregate's own declared tenant field is whichever
157
+ # field one of its own queries names in `authorize policy, tenant:
158
+ # :field` — the exact same declaration `authorize_scopes_or_
159
+ # refuses` reads off a query above, reused here to name a field
160
+ # on the aggregate itself that stores the tenant it belongs to.
161
+ # For every stored record (`history[:instances]` — a refused
162
+ # dispatch never writes one, so "a refusal is correct behaviour,
163
+ # not a finding" holds by construction, the same way `history
164
+ # [:instances]` already guarantees this for `stored_records_
165
+ # satisfy_declared_invariants`) whose own aggregate declares a
166
+ # tenant field, walk every `reference_to`-typed attribute it
167
+ # carries (`Bluebook::Reference` — "a reference IS the id",
168
+ # value/coercion.rb's own header, so the stored value is always a
169
+ # plain id, never a nested payload) pointing at another aggregate
170
+ # that also declares a tenant field: if the referenced record's
171
+ # own tenant value disagrees with the referencing record's own
172
+ # tenant value, the write crossed a tenant boundary and nothing
173
+ # refused it — a finding.
174
+ #
175
+ # A dangling/unresolvable reference is skipped — a different,
176
+ # existence-shaped property's claim, not this one's (the same
177
+ # "inconclusive, not a claimed pass" restraint `lifecycle_guard_
178
+ # and_given_violations_are_refused` already documents for a
179
+ # differently-shaped case). Comparison goes through `Ports::
180
+ # Query::InMemory.comparable` (the same normalization `authorize_
181
+ # scopes_or_refuses` already applies to a query row's own tenant
182
+ # field, just above) rather than `Runtime::Value#==` directly —
183
+ # two single-attribute value objects with the same scalar but
184
+ # different declared names (`LedgerRegion`/`TransferRegion`, this
185
+ # domain's own pair — a value object is always declared inside
186
+ # the aggregate that owns it, so two independently tenant-scoped
187
+ # aggregates can never share one) compare unequal under `Value#==`
188
+ # (`type_name` is part of that equality) despite meaning the
189
+ # identical tenant, which would make every same-tenant write a
190
+ # false positive.
191
+ # rubocop:disable-next Metrics/CyclomaticComplexity
192
+ # rubocop:disable-next Metrics/PerceivedComplexity
193
+ def commands_respect_tenant_scope(history)
194
+ bluebooks = history.fetch(:bluebooks)
195
+ instances = history.fetch(:instances)
196
+
197
+ offenders = instances.flat_map do |key, state|
198
+ domain_name = key.split("::").first
199
+ aggregate_name = key.split("::").last.split("#").first
200
+ aggregate = bluebooks[domain_name]&.aggregate(aggregate_name)
201
+ next [] unless aggregate
202
+
203
+ own_tenant_field = tenant_field_for(aggregate)
204
+ next [] unless own_tenant_field && state.key?(own_tenant_field)
205
+
206
+ own_tenant = Ports::Query::InMemory.comparable(state[own_tenant_field])
207
+
208
+ aggregate.attributes.filter_map do |attribute|
209
+ next unless attribute.type.is_a?(Bluebook::Reference)
210
+
211
+ target = bluebooks[domain_name]&.aggregate(attribute.type.target_name)
212
+ target_tenant_field = target && tenant_field_for(target)
213
+ next unless target_tenant_field
214
+
215
+ target_id = state[attribute.name]
216
+ next unless target_id
217
+
218
+ target_state = instances["#{domain_name}::#{target.name}##{target_id}"]
219
+ next unless target_state&.key?(target_tenant_field)
220
+
221
+ target_tenant = Ports::Query::InMemory.comparable(target_state[target_tenant_field])
222
+ next if target_tenant == own_tenant
223
+
224
+ "#{key} (#{own_tenant_field}: #{own_tenant.inspect}) references #{attribute.name}: #{target_id.inspect}, " \
225
+ "but #{domain_name}::#{target.name}##{target_id} carries #{target_tenant_field}: " \
226
+ "#{target_tenant.inspect} — a cross-tenant write nothing refused"
227
+ end
228
+ end
229
+
230
+ offenders.empty? || offenders.join("; ")
231
+ end
232
+
233
+ # The field an aggregate's own query names as tenant-scoping —
234
+ # shared by `commands_respect_tenant_scope` above for both sides
235
+ # of a `reference_to`. `nil` for an aggregate with no `authorize
236
+ # ..., tenant:` on any of its own queries — not every aggregate
237
+ # is tenant-scoped, and one that isn't has nothing for this
238
+ # property to check either side of.
239
+ def tenant_field_for(aggregate)
240
+ authorization = aggregate.queries.filter_map(&:authorization).find(&:tenant)
241
+ authorization&.tenant&.to_sym
242
+ end
243
+
244
+ # A declared process manager's own command — `command.hecks_name`,
142
245
  # or an entity's own if the verb's second component is itself
143
246
  # dotted (`Aggregate.Entity.Command`, the same two shapes
144
247
  # `Dispatcher#dispatch` itself branches on). Shared by the guard
145
248
  # property above and available for anything else that needs to go
146
249
  # from a replayed verb back to its declaration.
147
250
  #
148
- # RESOLVED AGAINST `bluebooks` (the FULL map, `history[:bluebooks]`
251
+ # Resolved against `bluebooks` (the full map, `history[:bluebooks]`
149
252
  # — every loaded domain, keyed by name), never a single assumed
150
- # bluebook: a verb names its OWN domain (`Naming.split_verb`'s
253
+ # bluebook: a verb names its own domain (`Naming.split_verb`'s
151
254
  # first element), and that domain is not always the one Replay
152
255
  # happens to expose as `history[:bluebook]`. A fuzz run against
153
256
  # `lib/hecks/grammar` (Expression + Translation, in load
@@ -155,14 +258,14 @@ module Hecks
155
258
  # refusal read as "no declared command resolves that verb" purely
156
259
  # because `history[:bluebook]` was Expression, not Translation; the
157
260
  # refusal was real, this property's own domain resolution was not.
158
- # A DELEGATING DOOR REFUSES WITH ITS TARGET'S OWN WORDS. `delegates_to`
261
+ # A delegating door refuses with its target's own words. `delegates_to`
159
262
  # (CommandBuilder#delegates_to_impl) hands the whole dispatch to one
160
263
  # entity command, and that command's given is what refuses — raised
161
264
  # back through the door, in the door's name (chess: `Game.MoveKnight
162
265
  # refused — "it is that color's turn"`, a given Knight.Move declares
163
266
  # and MoveKnight, a pure passthrough, never could). Read the door's
164
267
  # own guards first, then every delegation target's; an offence is
165
- # only a description NEITHER declares. Found live mining chess's
268
+ # only a description neither declares. Found live mining chess's
166
269
  # history: every refused move through a door read as undeclared.
167
270
  def effective_guard_descriptions(bluebooks, verb, command)
168
271
  own = command.guard_descriptions
@@ -193,9 +296,9 @@ module Hecks
193
296
  end
194
297
  end
195
298
 
196
- # `guard_refusals_are_declared`'s OWN OPPOSITE DIRECTION. That
299
+ # `guard_refusals_are_declared`'s own opposite direction. That
197
300
  # property is passive and one-directional — for a refusal that
198
- # ALREADY HAPPENED, is the quoted text real declared text? It says
301
+ # already happened, is the quoted text real declared text? It says
199
302
  # nothing about a guard that should have refused and silently did
200
303
  # not — a call site that stopped calling enforce_givens/enforce_
201
304
  # lifecycle_guard would never appear in history[:refusals] at all,
@@ -203,7 +306,7 @@ module Hecks
203
306
  #
204
307
  # This one calls Admissibility#enforce_givens (which itself folds
205
308
  # in #enforce_lifecycle_guard whenever `declaring:` is passed)
206
- # DIRECTLY, against Replay's own pre-dispatch snapshot
309
+ # directly, against Replay's own pre-dispatch snapshot
207
310
  # (history[:guard_checks], one bounded, additive extension — see
208
311
  # that file's own comment at the capture site) — an independent
209
312
  # recomputation, not grading production against itself, the same
@@ -213,27 +316,27 @@ module Hecks
213
316
  # could mutate anything a cross-aggregate given dereferences) is
214
317
  # compared against `actual_refused` (GivenNotMet/LifecycleRefused
215
318
  # specifically — Replay's own comment on GUARD_REFUSAL_CLASSES
216
- # explains why ANY other refusal class, or an outright success,
319
+ # explains why any other refusal class, or an outright success,
217
320
  # both count as "the guard did not fire," since enforce_givens
218
- # runs FIRST in DISPATCH_ORDER).
321
+ # runs first in DISPATCH_ORDER).
219
322
  #
220
323
  # Aggregate#preconditions closes for free alongside this — a
221
324
  # no-block `given` reference (CommandBuilder#given) pushes the
222
- # SAME Given struct object `enforce_givens` already iterates
325
+ # same Given struct object `enforce_givens` already iterates
223
326
  # command.givens for, so there is no separate runtime path a
224
327
  # property could exercise beyond what this already reaches.
225
328
  # Entity#preconditions closes the identical way, one level down
226
329
  # (ADR 0028) — a piece's own bare `given` reference pushes the
227
- # SAME Given struct onto ITS OWN referencing command's givens,
330
+ # same Given struct onto its own referencing command's givens,
228
331
  # so LedgerEntry's own Amend/Reverse (banking) already exercise
229
332
  # this through the exact mechanism above, no separate path.
230
333
  #
231
334
  # Real targets: Account.Debit/CloseAccount (`from:` guards),
232
335
  # Credit/Debit (the named-once `given("customer is active")`
233
336
  # precondition) — FreezeAccount deliberately references the
234
- # DIFFERENT named precondition `"customer is not closed"` instead
337
+ # different named precondition `"customer is not closed"` instead
235
338
  # (a suspended customer must still be freezable), so it is not a
236
- # `"customer is active"` example, just the same MECHANISM.
339
+ # `"customer is active"` example, just the same mechanism.
237
340
  def lifecycle_guard_and_given_violations_are_refused(history)
238
341
  offenders = history.fetch(:guard_checks).filter_map do |check|
239
342
  next if check[:recomputed_refused] == check[:actual_refused]
@@ -14,11 +14,11 @@ module Hecks
14
14
  # (#check_piece_invariants, #eligible_rows, #nest_rows,
15
15
  # #recompute_median) each leans on.
16
16
  module InvariantsAndAggregation
17
- # EVERY STORED RECORD STILL SATISFIES ITS OWN AGGREGATE'S DECLARED
18
- # INVARIANTS — Admissibility#enforce_invariants (command_rules/
19
- # admissibility.rb) checks these AFTER every command's mutations,
20
- # BEFORE save, the same point `ensures` is checked. Nothing until
21
- # now re-checked a record AFTER a whole replay finished, independent
17
+ # Every stored record still satisfies its own aggregate's declared
18
+ # invariants — Admissibility#enforce_invariants (command_rules/
19
+ # admissibility.rb) checks these after every command's mutations,
20
+ # before save, the same point `ensures` is checked. Nothing until
21
+ # now re-checked a record after a whole replay finished, independent
22
22
  # of whichever call site was supposed to have refused a violation
23
23
  # in the first place — a record failing its own declared invariant
24
24
  # here is proof a violating write landed anyway: the call site
@@ -27,9 +27,9 @@ module Hecks
27
27
  #
28
28
  # `history[:instances]` entries are already plain, symbol-keyed
29
29
  # state Hashes (Replay.call's own `record.state`) — called against
30
- # Evaluator.call the SAME way ValueObject::Builder#build already
30
+ # Evaluator.call the same way ValueObject::Builder#build already
31
31
  # does for a VO's own invariants (value/coercion.rb), no GuardState
32
- # wrapper needed the way enforce_invariants' own LIVE call uses one
32
+ # wrapper needed the way enforce_invariants' own live call uses one
33
33
  # (GuardState exists for `parent.`/projected-field dereferencing
34
34
  # mid-dispatch; a stored record's own scalar fields need none of
35
35
  # that to re-check a same-aggregate invariant against itself).
@@ -39,8 +39,8 @@ module Hecks
39
39
  #
40
40
  # Entity#invariants (round 7) closes here too, not for free —
41
41
  # `stored_records_satisfy_declared_invariants` only ever checked
42
- # the AGGREGATE's own flat state; a piece's own invariant is
43
- # checked against every ELEMENT of a `list_of` field, a genuinely
42
+ # the aggregate's own flat state; a piece's own invariant is
43
+ # checked against every element of a `list_of` field, a genuinely
44
44
  # different walk `check_piece_invariants` below makes,
45
45
  # independently of `Admissibility#check_entity_invariants` (the
46
46
  # live enforcement path this property exists to catch drifting
@@ -70,11 +70,11 @@ module Hecks
70
70
  offenders.empty? || offenders.join("; ")
71
71
  end
72
72
 
73
- # A PIECE'S OWN INVARIANT, checked against every element a
74
- # `list_of` field holds — the SAME lookup `Admissibility#
73
+ # A piece's own invariant, checked against every element a
74
+ # `list_of` field holds — the same lookup `Admissibility#
75
75
  # check_entity_invariants` makes (`owner.attributes.find { |a|
76
76
  # a.list? && a.type.to_s == entity.hecks_name }`), independently
77
- # reapplied here against a STORED record's own plain Hash state
77
+ # reapplied here against a stored record's own plain Hash state
78
78
  # rather than a live `Instance`.
79
79
  def check_piece_invariants(owner_construct, owner_state, key)
80
80
  owner_construct.entities.each do |entity|
@@ -99,23 +99,23 @@ module Hecks
99
99
  nil
100
100
  end
101
101
 
102
- # A SAGA INSTANCE'S OWN CHECKPOINT SURVIVES BEING WRITTEN AND READ
103
- # BACK — the durability contract `SagaInterpreter#checkpoint` makes
102
+ # A saga instance's own checkpoint survives being written and read
103
+ # back — the durability contract `SagaInterpreter#checkpoint` makes
104
104
  # (`state:` plus a `deep_copy`d `memory:`, handed to whatever
105
105
  # adapter answers `save_saga`) and `Registry#rehydrate_sagas!`
106
106
  # promises to restore on the next boot (`each_saga` yielding
107
107
  # `[pm, correlation, state, memory]` back into `saga_instances`).
108
- # `Replay` captures the LIVE store already materialised the same
108
+ # `Replay` captures the live store already materialised the same
109
109
  # way `checkpoint` itself does (`Value.materialize`, not raw
110
110
  # `Runtime::Value`s — see its own comment); this property pushes
111
- # that captured memory through the SAME `JSON.generate` then
111
+ # that captured memory through the same `JSON.generate` then
112
112
  # `JSON.parse(symbolize_names: true)` round-trip `checkpoint`'s own
113
113
  # `deep_copy` performs (mirrored here rather than called — a
114
114
  # private instance method with no registry to hand it) and checks
115
115
  # it comes back byte-identical. A memory holding anything that
116
116
  # round-trip cannot carry faithfully — a bare Symbol leaf, a
117
117
  # non-JSON type a future field introduces — is corruption the
118
- # durable path would introduce on a REAL restart, caught here
118
+ # durable path would introduce on a real restart, caught here
119
119
  # without needing one.
120
120
  #
121
121
  # `declares_state?` (Behaviour::ProcessManager) is the other half:
@@ -150,11 +150,11 @@ module Hecks
150
150
  offenders.empty? || offenders.join("; ")
151
151
  end
152
152
 
153
- # A `for_each` POLICY DISPATCHES EXACTLY ONCE PER ROW ITS DECLARED
154
- # QUERY ANSWERS — never once for the triggering event regardless of
153
+ # A `for_each` policy dispatches exactly once per row its declared
154
+ # query answers — never once for the triggering event regardless of
155
155
  # row count, never skipping a matched row, never firing on a row a
156
- # concurrent mutation only made match AFTER the fact. `Replay`
157
- # computes the expected row-id set INDEPENDENTLY, at the same
156
+ # concurrent mutation only made match after the fact. `Replay`
157
+ # computes the expected row-id set independently, at the same
158
158
  # instant the real dispatch runs (`Replay.expected_fan_out_rows`,
159
159
  # the query oracle's own shape aimed at fan-out: two engines
160
160
  # compared, never one graded against itself), and records it
@@ -183,21 +183,21 @@ module Hecks
183
183
  offenders.empty? || offenders.join("; ")
184
184
  end
185
185
 
186
- # A `count`/`median` REPORT'S REDUCED SCALAR MATCHES THE SAME
187
- # REDUCTION DONE INDEPENDENTLY, over the SAME eligible rows —
186
+ # A `count`/`median` report's reduced scalar matches the same
187
+ # reduction done independently, over the same eligible rows —
188
188
  # `ReadModelInterpreter#project`'s own FK-join (root first, then
189
189
  # each many-side head matched against it) and `#median` (odd →
190
190
  # the true middle, even → the average of the two middles as a
191
191
  # Float, empty → `nil`; `count` is the filtered length, empty →
192
192
  # `0`), reproduced here in plain Ruby against `history[:instances]`
193
193
  # rather than a live registry — `FieldPath.dig` +
194
- # `Ports::Query::InMemory.comparable`/`.holds?` are the SAME two
194
+ # `Ports::Query::InMemory.comparable`/`.holds?` are the same two
195
195
  # calls the interpreter itself makes to read a field and judge a
196
196
  # `where`, called here rather than re-derived, so this oracle
197
197
  # cannot drift from what "read a field" or "a clause holds" mean
198
198
  # without the interpreter drifting the identical way.
199
199
  #
200
- # Only a report whose `:query` is answered by the SAME bluebook
200
+ # Only a report whose `:query` is answered by the same bluebook
201
201
  # `history[:bluebook]` carries (the bare `Domain.report_name`
202
202
  # form, `domain == bluebook.name`) is checked — the same "only
203
203
  # what we have the grammar for" scope `lifecycle_values_are_declared`
@@ -206,7 +206,7 @@ module Hecks
206
206
  # ("only a report answered by this bluebook, only a count/median
207
207
  # report, only one with a reduced many-side head") each gating the
208
208
  # next, ending in one independent recomputation compared against
209
- # the live answer. The guards are what make this oracle SCOPED
209
+ # the live answer. The guards are what make this oracle scoped
210
210
  # correctly, not incidental complexity — narrower than "every
211
211
  # branch reads as its own precondition."
212
212
  # rubocop:disable-next Metrics/CyclomaticComplexity
@@ -239,14 +239,14 @@ module Hecks
239
239
  end
240
240
 
241
241
  # `aggregation_matches_recompute`'s own shape, extended from
242
- # reducing a many-side head to a scalar (count/median) to NESTING
242
+ # reducing a many-side head to a scalar (count/median) to nesting
243
243
  # it — `ReadModelInterpreter#group_by_target`/`#nest`, reproduced
244
244
  # here in plain Ruby against `history[:instances]` the same way
245
245
  # `eligible_rows` already reproduces the FK-join and `where`
246
246
  # narrowing count/median share. `Value.materialize_unwrapped` is
247
- # the SAME call `#project` makes before nesting (a single-field
247
+ # the same call `#project` makes before nesting (a single-field
248
248
  # value object recurses to its bare scalar — a real grouping key
249
- # has to BE one) — called here rather than re-derived, so this
249
+ # has to be one) — called here rather than re-derived, so this
250
250
  # oracle cannot drift from what "the group key" means without the
251
251
  # interpreter drifting the identical way.
252
252
  #
@@ -298,17 +298,17 @@ module Hecks
298
298
  end
299
299
  end
300
300
 
301
- # THE ELIGIBLE ROWS a `count`/`median` head reduces — every
301
+ # The eligible rows a `count`/`median` head reduces — every
302
302
  # instance of the reduced head's own aggregate, FK-matched against
303
303
  # the report's root reference (if it has one; a rootless report has
304
304
  # none to match) exactly the way `ReadModelInterpreter#reference_fields`
305
305
  # finds the matching attribute, then narrowed by the report's own
306
- # `where` clauses via the SAME `InMemory.holds?` the interpreter's
306
+ # `where` clauses via the same `InMemory.holds?` the interpreter's
307
307
  # `execute` calls.
308
308
  def eligible_rows(bluebook, instances, domain, model, reduced_head, args)
309
309
  aggregate = bluebook.aggregate(reduced_head[:aggregate])
310
310
  prefix = "#{domain}::#{reduced_head[:aggregate]}#"
311
- # `id:` MERGED IN, the same `record.to_h` (`@state.merge(id:
311
+ # `id:` merged in, the same `record.to_h` (`@state.merge(id:
312
312
  # @id)`) every live head row carries — count/median never read
313
313
  # it, but group_by_matches_recompute's own independent nesting
314
314
  # does, the same way ReadModelInterpreter#row(record) = record.
@@ -1,3 +1,5 @@
1
+ require_relative "../nondeterministic"
2
+
1
3
  module Hecks
2
4
  module Fuzzing
3
5
  module Properties
@@ -12,11 +14,11 @@ module Hecks
12
14
  # trustworthy in the first place.
13
15
  module LifecycleAndReplay
14
16
  # Every lifecycle field a replay leaves an instance holding is one
15
- # of the aggregate's OWN declared states — the full set, not just
17
+ # of the aggregate's own declared states — the full set, not just
16
18
  # `Lifecycle#states`' default+targets (see ModelCheck.full_states'
17
19
  # own comment on that hole). The tie to M2 is direct: the model
18
- # checker proves which states a domain's OWN declarations can ever
19
- # produce ; this proves a REAL RUN never produced anything else —
20
+ # checker proves which states a domain's own declarations can ever
21
+ # produce ; this proves a real run never produced anything else —
20
22
  # a coercion bug, a stale string surviving a rename, a default
21
23
  # that drifted from the declared set, would all show up here as a
22
24
  # value nothing upstream would have predicted.
@@ -44,10 +46,10 @@ module Hecks
44
46
  end
45
47
 
46
48
  # Every saga advance a replay actually logged moved along an edge
47
- # the process manager DECLARED — `(from, to)` pairs that appear in
49
+ # the process manager declared — `(from, to)` pairs that appear in
48
50
  # `saga_log` with `advanced: true` must be a `(handler.from_state,
49
51
  # handler.to_state)` pair some handler on that PM declares
50
- # (compensation edges included ; a REFUSED-triggered advance is a
52
+ # (compensation edges included ; a refused-triggered advance is a
51
53
  # handler like any other). A saga that advanced along a pair no
52
54
  # handler names would mean the runtime moved state the language
53
55
  # never authorized — the same trust ModelCheck's static reachability
@@ -72,23 +74,41 @@ module Hecks
72
74
  offenders.empty? || offenders.join("; ")
73
75
  end
74
76
 
75
- # THE FOUNDATIONAL ONE. `Hecks::Runtime` mints nothing — every
77
+ # The foundational one. `Hecks::Runtime` mints nothing — every
76
78
  # identity is declared and derived, never invented (see
77
- # command_interpreter.rb's own "NOTHING IS MINTED" — a random hex,
79
+ # command_interpreter.rb's own "nothing is minted" — a random hex,
78
80
  # a counter, anything not reproducible from the payload, was
79
81
  # refused out of the runtime specifically because it broke this).
80
- # So the SAME steps, replayed against a FRESH boot, must produce
81
- # BYTE-IDENTICAL events, refusals, and instances — any drift here
82
+ # So the same steps, replayed against a fresh boot, must produce
83
+ # byte-identical events, refusals, and instances — any drift here
82
84
  # is nondeterminism the runtime promised not to have: a wall-clock
83
85
  # read that leaked into compared state, a Hash iteration order a
84
86
  # comparison depended on, anything. Two independent replays, not a
85
- # cached one compared to itself, so a bug that corrupts the FIRST
87
+ # cached one compared to itself, so a bug that corrupts the first
86
88
  # run's own bookkeeping cannot pass by agreeing with itself.
87
89
  def replay_is_deterministic(domain_path, steps, adapter: :memory)
88
90
  first = Replay.call(domain_path, steps, adapter: adapter)
89
91
  second = Replay.call(domain_path, steps, adapter: adapter)
90
92
 
91
- comparable = ->(history) { history.except(:bluebook, :bluebooks) }
93
+ # What leaves this comparison, and why, is declared once in
94
+ # `Nondeterministic::FIELDS`: the `outbox_row` group (per-enqueue
95
+ # uuids), the `event` group on each row's full `Event#to_h`
96
+ # (`occurred_at`, which `history[:events]` never carried), and the
97
+ # `history` group (live IR objects). Stripped per `outbox_traces`
98
+ # row rather than dropping `outbox_traces` wholesale: everything
99
+ # else on a row (status, consumer, kind, the event's own name/
100
+ # aggregate/id/payload) is reproducible from the steps alone and
101
+ # stays checked.
102
+ strip_outbox_nondeterminism = lambda do |history|
103
+ traces = Array(history[:outbox_traces]).map do |trace|
104
+ trace.merge(rows: trace[:rows].map do |row|
105
+ Nondeterministic.strip(row, :outbox_row).merge(event: Nondeterministic.strip(row[:event], :event))
106
+ end)
107
+ end
108
+ history.merge(outbox_traces: traces)
109
+ end
110
+
111
+ comparable = ->(history) { Nondeterministic.strip(strip_outbox_nondeterminism.call(history), :history) }
92
112
  return true if comparable.call(first) == comparable.call(second)
93
113
 
94
114
  "two replays of the same #{steps.length} steps produced different histories"