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
@@ -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.
@@ -54,6 +54,11 @@ module Hecks
54
54
  # branches shares logic with the others beyond that shared setup.
55
55
  # rubocop:disable-next Metrics/CyclomaticComplexity
56
56
  # rubocop:disable-next Metrics/PerceivedComplexity
57
+ #
58
+ # @param history [Hash] a replayed history as returned by `Replay.call`
59
+ # @return [true, String] true if every tenant-scoped query answer, and every
60
+ # refusal of one, agrees with `TenantScope.apply`'s own contract; otherwise a
61
+ # message naming the offending query, its args, and what it should have done
57
62
  def authorize_scopes_or_refuses(history)
58
63
  bluebooks = history.fetch(:bluebooks)
59
64
 
@@ -92,14 +97,14 @@ module Hecks
92
97
  offenders.empty? || offenders.join("; ")
93
98
  end
94
99
 
95
- # EVERY GIVEN/ENSURES REFUSAL A RUN ACTUALLY RAISED NAMES A RULE
96
- # THE COMMAND ACTUALLY DECLARES. `GivenNotMet`/`EnsuresNotMet` both
100
+ # Every given/ensures refusal a run actually raised names a rule
101
+ # the command actually declares. `GivenNotMet`/`EnsuresNotMet` both
97
102
  # quote their guard's own `description` verbatim
98
103
  # (command_rules/admissibility.rb: `"#{command.hecks_name} refused
99
- # — #{given.description}"`) — the SAME text `behavior.bluebook`'s
104
+ # — #{given.description}"`) — the same text `behavior.bluebook`'s
100
105
  # own `Rule`/Command.Ensure hold as `Rule#description`, so a
101
106
  # refusal whose quoted text is not among the refusing command's
102
- # OWN `guard_descriptions` (Behaviour::Command, both givens and
107
+ # own `guard_descriptions` (Behaviour::Command, both givens and
103
108
  # ensures) is either a stale message surviving a renamed rule, a
104
109
  # rule firing against the wrong command's own guard set, or the
105
110
  # wording drifting out from under the declaration it is supposed
@@ -107,7 +112,7 @@ module Hecks
107
112
  # guards, some through a cross-aggregate dereference) are exactly
108
113
  # the surface this exists to hold to its word.
109
114
  #
110
- # `kind:` is what tells a guard refusal apart from the FOUR other
115
+ # `kind:` is what tells a guard refusal apart from the four other
111
116
  # `RefusalWording` templates sharing the identical "X refused — Y"
112
117
  # shape (LifecycleRefused/transition_blocked, both TypeMismatch
113
118
  # object-reference templates, Unauthorized/role_mismatch) — see
@@ -116,6 +121,14 @@ module Hecks
116
121
  # those; the raised class does not.
117
122
  GUARD_REFUSAL_KINDS = %w[Hecks::Runtime::GivenNotMet Hecks::Runtime::EnsuresNotMet].freeze
118
123
 
124
+ # Checks that every given/ensures refusal a run actually raised names a rule
125
+ # the refusing command actually declares.
126
+ #
127
+ # @param history [Hash] a replayed history as returned by `Replay.call`
128
+ # @return [true, String] true if every guard refusal's quoted description matches
129
+ # a `given`/`ensures` the refusing command (or its delegation target) declares;
130
+ # otherwise a message naming the refusal and the declared descriptions it does
131
+ # not match
119
132
  def guard_refusals_are_declared(history)
120
133
  bluebooks = history.fetch(:bluebooks)
121
134
 
@@ -138,27 +151,27 @@ module Hecks
138
151
  offenders.empty? || offenders.join("; ")
139
152
  end
140
153
 
141
- # ANGLE-8's OWN WRITE-SIDE HALF. `authorize_scopes_or_refuses`
154
+ # Angle-8's own write-side half. `authorize_scopes_or_refuses`
142
155
  # (above) enforces `TenantScope.apply`'s boundary, and that
143
- # boundary exists ONLY for queries/read models — `authorize
156
+ # boundary exists only for queries/read models — `authorize
144
157
  # policy, tenant: field` is a word `QuerySpecification::Common::
145
158
  # DSL#authorize_impl` grants to `QueryBuilder`/`ReadModelBuilder`
146
159
  # alone; `CommandBuilder` never includes that module, so no
147
160
  # bluebook can declare it on a command at all (confirmed by
148
- # reading the grammar directly, not inferred). A WRITE that
161
+ # reading the grammar directly, not inferred). A write that
149
162
  # carries a `reference_to` from one tenant-scoped record into
150
- # another's is checked by NOTHING at dispatch time: `TenantScope`
163
+ # another's is checked by nothing at dispatch time: `TenantScope`
151
164
  # never runs for a command, and no runtime `given`/`ensures`
152
165
  # anywhere in this corpus reads a cross-aggregate tenant field
153
166
  # either. `qa/stress_domains/tenant_ledger` exists to give this
154
167
  # property a real place to fire.
155
168
  #
156
- # THE RULE: an aggregate's own declared TENANT FIELD is whichever
157
- # field one of ITS OWN queries names in `authorize policy, tenant:
169
+ # The rule: an aggregate's own declared tenant field is whichever
170
+ # field one of its own queries names in `authorize policy, tenant:
158
171
  # :field` — the exact same declaration `authorize_scopes_or_
159
172
  # 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
173
+ # on the aggregate itself that stores the tenant it belongs to.
174
+ # For every stored record (`history[:instances]` — a refused
162
175
  # dispatch never writes one, so "a refusal is correct behaviour,
163
176
  # not a finding" holds by construction, the same way `history
164
177
  # [:instances]` already guarantees this for `stored_records_
@@ -166,30 +179,36 @@ module Hecks
166
179
  # tenant field, walk every `reference_to`-typed attribute it
167
180
  # carries (`Bluebook::Reference` — "a reference IS the id",
168
181
  # 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
182
+ # plain id, never a nested payload) pointing at another aggregate
183
+ # that also declares a tenant field: if the referenced record's
171
184
  # own tenant value disagrees with the referencing record's own
172
185
  # tenant value, the write crossed a tenant boundary and nothing
173
186
  # refused it — a finding.
174
187
  #
175
- # A DANGLING/UNRESOLVABLE REFERENCE IS SKIPPED — a different,
188
+ # A dangling/unresolvable reference is skipped — a different,
176
189
  # existence-shaped property's claim, not this one's (the same
177
190
  # "inconclusive, not a claimed pass" restraint `lifecycle_guard_
178
191
  # and_given_violations_are_refused` already documents for a
179
192
  # differently-shaped case). Comparison goes through `Ports::
180
- # Query::InMemory.comparable` (the SAME normalization `authorize_
193
+ # Query::InMemory.comparable` (the same normalization `authorize_
181
194
  # scopes_or_refuses` already applies to a query row's own tenant
182
195
  # 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
196
+ # two single-attribute value objects with the same scalar but
197
+ # different declared names (`LedgerRegion`/`TransferRegion`, this
185
198
  # domain's own pair — a value object is always declared inside
186
199
  # the aggregate that owns it, so two independently tenant-scoped
187
- # aggregates can never share one) compare UNEQUAL under `Value#==`
200
+ # aggregates can never share one) compare unequal under `Value#==`
188
201
  # (`type_name` is part of that equality) despite meaning the
189
202
  # identical tenant, which would make every same-tenant write a
190
203
  # false positive.
191
204
  # rubocop:disable-next Metrics/CyclomaticComplexity
192
205
  # rubocop:disable-next Metrics/PerceivedComplexity
206
+ #
207
+ # @param history [Hash] a replayed history as returned by `Replay.call`
208
+ # @return [true, String] true if every stored record's `reference_to` attribute
209
+ # points at a record sharing the same tenant value (or has no resolvable
210
+ # target, or neither side declares a tenant field); otherwise a message naming
211
+ # the referencing record, the reference, and the tenant mismatch
193
212
  def commands_respect_tenant_scope(history)
194
213
  bluebooks = history.fetch(:bluebooks)
195
214
  instances = history.fetch(:instances)
@@ -230,27 +249,31 @@ module Hecks
230
249
  offenders.empty? || offenders.join("; ")
231
250
  end
232
251
 
233
- # THE FIELD AN AGGREGATE'S OWN QUERY NAMES AS TENANT-SCOPING —
252
+ # The field an aggregate's own query names as tenant-scoping —
234
253
  # shared by `commands_respect_tenant_scope` above for both sides
235
254
  # of a `reference_to`. `nil` for an aggregate with no `authorize
236
255
  # ..., tenant:` on any of its own queries — not every aggregate
237
256
  # is tenant-scoped, and one that isn't has nothing for this
238
257
  # property to check either side of.
258
+ #
259
+ # @param aggregate [Bluebook::Aggregate] the aggregate to look up
260
+ # @return [Symbol, nil] the tenant field name, or nil if no query on the
261
+ # aggregate declares `authorize ..., tenant:`
239
262
  def tenant_field_for(aggregate)
240
263
  authorization = aggregate.queries.filter_map(&:authorization).find(&:tenant)
241
264
  authorization&.tenant&.to_sym
242
265
  end
243
266
 
244
- # A DECLARED PROCESS MANAGER'S OWN COMMAND — `command.hecks_name`,
267
+ # A declared process manager's own command — `command.hecks_name`,
245
268
  # or an entity's own if the verb's second component is itself
246
269
  # dotted (`Aggregate.Entity.Command`, the same two shapes
247
270
  # `Dispatcher#dispatch` itself branches on). Shared by the guard
248
271
  # property above and available for anything else that needs to go
249
272
  # from a replayed verb back to its declaration.
250
273
  #
251
- # RESOLVED AGAINST `bluebooks` (the FULL map, `history[:bluebooks]`
274
+ # Resolved against `bluebooks` (the full map, `history[:bluebooks]`
252
275
  # — every loaded domain, keyed by name), never a single assumed
253
- # bluebook: a verb names its OWN domain (`Naming.split_verb`'s
276
+ # bluebook: a verb names its own domain (`Naming.split_verb`'s
254
277
  # first element), and that domain is not always the one Replay
255
278
  # happens to expose as `history[:bluebook]`. A fuzz run against
256
279
  # `lib/hecks/grammar` (Expression + Translation, in load
@@ -258,15 +281,23 @@ module Hecks
258
281
  # refusal read as "no declared command resolves that verb" purely
259
282
  # because `history[:bluebook]` was Expression, not Translation; the
260
283
  # refusal was real, this property's own domain resolution was not.
261
- # A DELEGATING DOOR REFUSES WITH ITS TARGET'S OWN WORDS. `delegates_to`
284
+ # A delegating door refuses with its target's own words. `delegates_to`
262
285
  # (CommandBuilder#delegates_to_impl) hands the whole dispatch to one
263
286
  # entity command, and that command's given is what refuses — raised
264
287
  # back through the door, in the door's name (chess: `Game.MoveKnight
265
288
  # refused — "it is that color's turn"`, a given Knight.Move declares
266
289
  # and MoveKnight, a pure passthrough, never could). Read the door's
267
290
  # own guards first, then every delegation target's; an offence is
268
- # only a description NEITHER declares. Found live mining chess's
291
+ # only a description neither declares. Found live mining chess's
269
292
  # history: every refused move through a door read as undeclared.
293
+ #
294
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
295
+ # keyed by domain name
296
+ # @param verb [String] the dispatched verb, `"Domain::Aggregate.Command"` or
297
+ # `"Domain::Aggregate.Entity.Command"`
298
+ # @param command [Bluebook::Command] the command `verb` resolves to
299
+ # @return [Array<String>] `command`'s own declared given/ensures descriptions,
300
+ # plus every `delegate`d command's, in that order
270
301
  def effective_guard_descriptions(bluebooks, verb, command)
271
302
  own = command.guard_descriptions
272
303
  delegated = command.mutations.select { |m| m.op == :delegate }.flat_map do |delegation|
@@ -277,6 +308,14 @@ module Hecks
277
308
  own + delegated
278
309
  end
279
310
 
311
+ # Resolves a dispatched verb back to the declared command it names.
312
+ #
313
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
314
+ # keyed by domain name
315
+ # @param verb [String] the dispatched verb, `"Domain::Aggregate.Command"` or
316
+ # `"Domain::Aggregate.Entity.Command"`
317
+ # @return [Bluebook::Command, nil] the declared command, or nil if `verb`'s
318
+ # domain, aggregate, entity, or command is not found among `bluebooks`
280
319
  def command_for_verb(bluebooks, verb)
281
320
  domain, aggregate_name, command_path = Naming.split_verb(verb)
282
321
  return nil unless command_path
@@ -296,9 +335,9 @@ module Hecks
296
335
  end
297
336
  end
298
337
 
299
- # `guard_refusals_are_declared`'s OWN OPPOSITE DIRECTION. That
338
+ # `guard_refusals_are_declared`'s own opposite direction. That
300
339
  # property is passive and one-directional — for a refusal that
301
- # ALREADY HAPPENED, is the quoted text real declared text? It says
340
+ # already happened, is the quoted text real declared text? It says
302
341
  # nothing about a guard that should have refused and silently did
303
342
  # not — a call site that stopped calling enforce_givens/enforce_
304
343
  # lifecycle_guard would never appear in history[:refusals] at all,
@@ -306,7 +345,7 @@ module Hecks
306
345
  #
307
346
  # This one calls Admissibility#enforce_givens (which itself folds
308
347
  # in #enforce_lifecycle_guard whenever `declaring:` is passed)
309
- # DIRECTLY, against Replay's own pre-dispatch snapshot
348
+ # directly, against Replay's own pre-dispatch snapshot
310
349
  # (history[:guard_checks], one bounded, additive extension — see
311
350
  # that file's own comment at the capture site) — an independent
312
351
  # recomputation, not grading production against itself, the same
@@ -316,27 +355,32 @@ module Hecks
316
355
  # could mutate anything a cross-aggregate given dereferences) is
317
356
  # compared against `actual_refused` (GivenNotMet/LifecycleRefused
318
357
  # specifically — Replay's own comment on GUARD_REFUSAL_CLASSES
319
- # explains why ANY other refusal class, or an outright success,
358
+ # explains why any other refusal class, or an outright success,
320
359
  # both count as "the guard did not fire," since enforce_givens
321
- # runs FIRST in DISPATCH_ORDER).
360
+ # runs first in DISPATCH_ORDER).
322
361
  #
323
362
  # Aggregate#preconditions closes for free alongside this — a
324
363
  # no-block `given` reference (CommandBuilder#given) pushes the
325
- # SAME Given struct object `enforce_givens` already iterates
364
+ # same Given struct object `enforce_givens` already iterates
326
365
  # command.givens for, so there is no separate runtime path a
327
366
  # property could exercise beyond what this already reaches.
328
367
  # Entity#preconditions closes the identical way, one level down
329
368
  # (ADR 0028) — a piece's own bare `given` reference pushes the
330
- # SAME Given struct onto ITS OWN referencing command's givens,
369
+ # same Given struct onto its own referencing command's givens,
331
370
  # so LedgerEntry's own Amend/Reverse (banking) already exercise
332
371
  # this through the exact mechanism above, no separate path.
333
372
  #
334
373
  # Real targets: Account.Debit/CloseAccount (`from:` guards),
335
374
  # Credit/Debit (the named-once `given("customer is active")`
336
375
  # precondition) — FreezeAccount deliberately references the
337
- # DIFFERENT named precondition `"customer is not closed"` instead
376
+ # different named precondition `"customer is not closed"` instead
338
377
  # (a suspended customer must still be freezable), so it is not a
339
- # `"customer is active"` example, just the same MECHANISM.
378
+ # `"customer is active"` example, just the same mechanism.
379
+ #
380
+ # @param history [Hash] a replayed history as returned by `Replay.call`
381
+ # @return [true, String] true if every recomputed `enforce_givens`/
382
+ # `enforce_lifecycle_guard` check agrees with what the real dispatch did;
383
+ # otherwise a message naming the verb and the disagreement
340
384
  def lifecycle_guard_and_given_violations_are_refused(history)
341
385
  offenders = history.fetch(:guard_checks).filter_map do |check|
342
386
  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
@@ -49,6 +49,11 @@ module Hecks
49
49
  #
50
50
  # Real target: SafeDepositBox's own Visit — `invariant("a written
51
51
  # note is not blank") { !note || !note.text.to_s.empty? }`.
52
+ #
53
+ # @param history [Hash] a replayed history as returned by `Replay.call`
54
+ # @return [true, String] true if every stored record satisfies its own
55
+ # (and its own entities') declared invariants; otherwise a message
56
+ # naming the record and the invariant it violates
52
57
  def stored_records_satisfy_declared_invariants(history)
53
58
  bluebooks = history.fetch(:bluebooks)
54
59
 
@@ -70,12 +75,18 @@ module Hecks
70
75
  offenders.empty? || offenders.join("; ")
71
76
  end
72
77
 
73
- # A PIECE'S OWN INVARIANT, checked against every element a
74
- # `list_of` field holds — the SAME lookup `Admissibility#
78
+ # A piece's own invariant, checked against every element a
79
+ # `list_of` field holds — the same lookup `Admissibility#
75
80
  # check_entity_invariants` makes (`owner.attributes.find { |a|
76
81
  # a.list? && a.type.to_s == entity.hecks_name }`), independently
77
- # reapplied here against a STORED record's own plain Hash state
82
+ # reapplied here against a stored record's own plain Hash state
78
83
  # rather than a live `Instance`.
84
+ # @param owner_construct [Bluebook::Aggregate, Bluebook::Entity] the
85
+ # construct whose own `list_of` entities to check
86
+ # @param owner_state [Hash] `owner_construct`'s own stored state
87
+ # @param key [String] the top-level record key, for the message
88
+ # @return [String, nil] a message naming the first violating piece found,
89
+ # at any nesting depth; `nil` if every piece satisfies its invariants
79
90
  def check_piece_invariants(owner_construct, owner_state, key)
80
91
  owner_construct.entities.each do |entity|
81
92
  next if entity.invariants.empty?
@@ -99,29 +110,33 @@ module Hecks
99
110
  nil
100
111
  end
101
112
 
102
- # A SAGA INSTANCE'S OWN CHECKPOINT SURVIVES BEING WRITTEN AND READ
103
- # BACK — the durability contract `SagaInterpreter#checkpoint` makes
113
+ # A saga instance's own checkpoint survives being written and read
114
+ # back — the durability contract `SagaInterpreter#checkpoint` makes
104
115
  # (`state:` plus a `deep_copy`d `memory:`, handed to whatever
105
116
  # adapter answers `save_saga`) and `Registry#rehydrate_sagas!`
106
117
  # promises to restore on the next boot (`each_saga` yielding
107
118
  # `[pm, correlation, state, memory]` back into `saga_instances`).
108
- # `Replay` captures the LIVE store already materialised the same
119
+ # `Replay` captures the live store already materialised the same
109
120
  # way `checkpoint` itself does (`Value.materialize`, not raw
110
121
  # `Runtime::Value`s — see its own comment); this property pushes
111
- # that captured memory through the SAME `JSON.generate` then
122
+ # that captured memory through the same `JSON.generate` then
112
123
  # `JSON.parse(symbolize_names: true)` round-trip `checkpoint`'s own
113
124
  # `deep_copy` performs (mirrored here rather than called — a
114
125
  # private instance method with no registry to hand it) and checks
115
126
  # it comes back byte-identical. A memory holding anything that
116
127
  # round-trip cannot carry faithfully — a bare Symbol leaf, a
117
128
  # non-JSON type a future field introduces — is corruption the
118
- # durable path would introduce on a REAL restart, caught here
129
+ # durable path would introduce on a real restart, caught here
119
130
  # without needing one.
120
131
  #
121
132
  # `declares_state?` (Behaviour::ProcessManager) is the other half:
122
133
  # a live or rehydrated instance sitting in a state the procedure
123
134
  # never declares is the saga-durability twin of
124
135
  # `lifecycle_values_are_declared` above.
136
+ # @param history [Hash] a replayed history as returned by `Replay.call`
137
+ # @return [true, String] true if every saga instance holds a declared
138
+ # state and its own memory survives a checkpoint round-trip; otherwise a
139
+ # message naming the process manager, correlation, and problem
125
140
  def sagas_rehydrate_cleanly(history)
126
141
  bluebook = history.fetch(:bluebook)
127
142
  process_managers = bluebook.process_managers.to_h { |pm| [pm.name, pm] }
@@ -150,11 +165,11 @@ module Hecks
150
165
  offenders.empty? || offenders.join("; ")
151
166
  end
152
167
 
153
- # A `for_each` POLICY DISPATCHES EXACTLY ONCE PER ROW ITS DECLARED
154
- # QUERY ANSWERS — never once for the triggering event regardless of
168
+ # A `for_each` policy dispatches exactly once per row its declared
169
+ # query answers — never once for the triggering event regardless of
155
170
  # 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
171
+ # concurrent mutation only made match after the fact. `Replay`
172
+ # computes the expected row-id set independently, at the same
158
173
  # instant the real dispatch runs (`Replay.expected_fan_out_rows`,
159
174
  # the query oracle's own shape aimed at fan-out: two engines
160
175
  # compared, never one graded against itself), and records it
@@ -163,6 +178,10 @@ module Hecks
163
178
  # dispatch is the claim then, not "dispatched to zero rows," and a
164
179
  # policy that dispatched anyway despite a failing guard is as real
165
180
  # a finding as a row it skipped.
181
+ # @param history [Hash] a replayed history as returned by `Replay.call`
182
+ # @return [true, String] true if every fan-out finding's actual dispatches
183
+ # match its independently computed expected row set; otherwise a message
184
+ # naming the policy, event, and disagreement
166
185
  def fanout_dispatches_once_per_matching_row(history)
167
186
  offenders = history.fetch(:fan_outs).filter_map do |finding|
168
187
  expected = finding[:expected_row_ids]
@@ -183,21 +202,21 @@ module Hecks
183
202
  offenders.empty? || offenders.join("; ")
184
203
  end
185
204
 
186
- # A `count`/`median` REPORT'S REDUCED SCALAR MATCHES THE SAME
187
- # REDUCTION DONE INDEPENDENTLY, over the SAME eligible rows —
205
+ # A `count`/`median` report's reduced scalar matches the same
206
+ # reduction done independently, over the same eligible rows —
188
207
  # `ReadModelInterpreter#project`'s own FK-join (root first, then
189
208
  # each many-side head matched against it) and `#median` (odd →
190
209
  # the true middle, even → the average of the two middles as a
191
210
  # Float, empty → `nil`; `count` is the filtered length, empty →
192
211
  # `0`), reproduced here in plain Ruby against `history[:instances]`
193
212
  # rather than a live registry — `FieldPath.dig` +
194
- # `Ports::Query::InMemory.comparable`/`.holds?` are the SAME two
213
+ # `Ports::Query::InMemory.comparable`/`.holds?` are the same two
195
214
  # calls the interpreter itself makes to read a field and judge a
196
215
  # `where`, called here rather than re-derived, so this oracle
197
216
  # cannot drift from what "read a field" or "a clause holds" mean
198
217
  # without the interpreter drifting the identical way.
199
218
  #
200
- # Only a report whose `:query` is answered by the SAME bluebook
219
+ # Only a report whose `:query` is answered by the same bluebook
201
220
  # `history[:bluebook]` carries (the bare `Domain.report_name`
202
221
  # form, `domain == bluebook.name`) is checked — the same "only
203
222
  # what we have the grammar for" scope `lifecycle_values_are_declared`
@@ -206,11 +225,15 @@ module Hecks
206
225
  # ("only a report answered by this bluebook, only a count/median
207
226
  # report, only one with a reduced many-side head") each gating the
208
227
  # next, ending in one independent recomputation compared against
209
- # the live answer. The guards are what make this oracle SCOPED
228
+ # the live answer. The guards are what make this oracle scoped
210
229
  # correctly, not incidental complexity — narrower than "every
211
230
  # branch reads as its own precondition."
212
231
  # rubocop:disable-next Metrics/CyclomaticComplexity
213
232
  # rubocop:disable-next Metrics/PerceivedComplexity
233
+ # @param history [Hash] a replayed history as returned by `Replay.call`
234
+ # @return [true, String] true if every eligible count/median report answer
235
+ # matches an independent recomputation; otherwise a message naming the
236
+ # query and the disagreement
214
237
  def aggregation_matches_recompute(history)
215
238
  bluebook = history.fetch(:bluebook)
216
239
 
@@ -239,14 +262,14 @@ module Hecks
239
262
  end
240
263
 
241
264
  # `aggregation_matches_recompute`'s own shape, extended from
242
- # reducing a many-side head to a scalar (count/median) to NESTING
265
+ # reducing a many-side head to a scalar (count/median) to nesting
243
266
  # it — `ReadModelInterpreter#group_by_target`/`#nest`, reproduced
244
267
  # here in plain Ruby against `history[:instances]` the same way
245
268
  # `eligible_rows` already reproduces the FK-join and `where`
246
269
  # narrowing count/median share. `Value.materialize_unwrapped` is
247
- # the SAME call `#project` makes before nesting (a single-field
270
+ # the same call `#project` makes before nesting (a single-field
248
271
  # value object recurses to its bare scalar — a real grouping key
249
- # has to BE one) — called here rather than re-derived, so this
272
+ # has to be one) — called here rather than re-derived, so this
250
273
  # oracle cannot drift from what "the group key" means without the
251
274
  # interpreter drifting the identical way.
252
275
  #
@@ -257,6 +280,11 @@ module Hecks
257
280
  # of count/median, nest_rows in place of recompute_median.
258
281
  # rubocop:disable-next Metrics/CyclomaticComplexity
259
282
  # rubocop:disable-next Metrics/PerceivedComplexity
283
+ #
284
+ # @param history [Hash] a replayed history as returned by `Replay.call`
285
+ # @return [true, String] true if every eligible group_by report answer matches
286
+ # an independent recompute; otherwise a semicolon-joined message naming each
287
+ # offending query
260
288
  def group_by_matches_recompute(history)
261
289
  bluebook = history.fetch(:bluebook)
262
290
 
@@ -290,6 +318,11 @@ module Hecks
290
318
  # per `group_by` field in declared order, leaf is the row with
291
319
  # every grouped field stripped (already spent, as the keys that
292
320
  # reached it).
321
+ # @param rows [Array<Hash>] materialized, symbol-keyed rows to nest
322
+ # @param fields [Array<Symbol>] the `group_by` fields, in declared order
323
+ # @return [Hash] one level of nesting per field, in order; the leaf under
324
+ # each key path is the first row in that group with every grouped field
325
+ # stripped
293
326
  def nest_rows(rows, fields)
294
327
  field, *rest = fields
295
328
  rows.group_by { |row| row[field] }.transform_values do |group|
@@ -298,17 +331,27 @@ module Hecks
298
331
  end
299
332
  end
300
333
 
301
- # THE ELIGIBLE ROWS a `count`/`median` head reduces — every
334
+ # The eligible rows a `count`/`median` head reduces — every
302
335
  # instance of the reduced head's own aggregate, FK-matched against
303
336
  # the report's root reference (if it has one; a rootless report has
304
337
  # none to match) exactly the way `ReadModelInterpreter#reference_fields`
305
338
  # finds the matching attribute, then narrowed by the report's own
306
- # `where` clauses via the SAME `InMemory.holds?` the interpreter's
339
+ # `where` clauses via the same `InMemory.holds?` the interpreter's
307
340
  # `execute` calls.
341
+ # @param bluebook [Bluebook::Chapter] the bluebook the report belongs to
342
+ # @param instances [Hash] the snapshot to read rows from (`history[:instances]`
343
+ # shape, or the query's own `instances_at`)
344
+ # @param domain [String] the domain name the reduced aggregate belongs to
345
+ # @param model [Bluebook::ReadModel] the report being recomputed
346
+ # @param reduced_head [Hash] the many-side aggregate head being reduced, from
347
+ # `model.aggregate_heads`
348
+ # @param args [Hash] the query's own arguments, for FK matching and `where`
349
+ # evaluation
350
+ # @return [Array<Hash>] every eligible row's own state, `id:` merged in
308
351
  def eligible_rows(bluebook, instances, domain, model, reduced_head, args)
309
352
  aggregate = bluebook.aggregate(reduced_head[:aggregate])
310
353
  prefix = "#{domain}::#{reduced_head[:aggregate]}#"
311
- # `id:` MERGED IN, the same `record.to_h` (`@state.merge(id:
354
+ # `id:` merged in, the same `record.to_h` (`@state.merge(id:
312
355
  # @id)`) every live head row carries — count/median never read
313
356
  # it, but group_by_matches_recompute's own independent nesting
314
357
  # does, the same way ReadModelInterpreter#row(record) = record.
@@ -337,6 +380,11 @@ module Hecks
337
380
  # → the average of the two middle values, as a Float; empty → nil,
338
381
  # never zero, so a caller cannot mistake "nothing to average" for
339
382
  # "averaged to zero."
383
+ # @param rows [Array<Hash>] eligible rows, as returned by `#eligible_rows`
384
+ # @param field [Symbol] the field to average
385
+ # @return [Object, Float, nil] the true middle value for an odd count, the
386
+ # Float average of the two middle values for an even count, or `nil` for
387
+ # an empty `rows`
340
388
  def recompute_median(rows, field)
341
389
  values = rows.map { |state| Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, field)) }
342
390
  .compact.sort