hecks 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -1,3 +1,5 @@
1
+ require_relative "../nondeterministic"
2
+
1
3
  module Hecks
2
4
  module Fuzzing
3
5
  module Properties
@@ -12,14 +14,18 @@ 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.
25
+ #
26
+ # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
27
+ # @return [true, String] true if every instance's lifecycle field holds a declared
28
+ # state; otherwise a semicolon-joined message naming each offending instance
23
29
  def lifecycle_values_are_declared(history)
24
30
  bluebook = history.fetch(:bluebook)
25
31
  declared = {}
@@ -44,14 +50,19 @@ module Hecks
44
50
  end
45
51
 
46
52
  # Every saga advance a replay actually logged moved along an edge
47
- # the process manager DECLARED — `(from, to)` pairs that appear in
53
+ # the process manager declared — `(from, to)` pairs that appear in
48
54
  # `saga_log` with `advanced: true` must be a `(handler.from_state,
49
55
  # handler.to_state)` pair some handler on that PM declares
50
- # (compensation edges included ; a REFUSED-triggered advance is a
56
+ # (compensation edges included ; a refused-triggered advance is a
51
57
  # handler like any other). A saga that advanced along a pair no
52
58
  # handler names would mean the runtime moved state the language
53
59
  # never authorized — the same trust ModelCheck's static reachability
54
60
  # rests on, checked here against what a run actually did.
61
+ #
62
+ # @param history [Hash] a replayed history as returned by `Replay.call`
63
+ # @return [true, String] true if every logged advance matches a declared
64
+ # handler edge; otherwise a message naming the process manager and pair
65
+ # that does not
55
66
  def saga_advances_follow_declared_handlers(history)
56
67
  bluebook = history.fetch(:bluebook)
57
68
  edges = Hash.new { |h, k| h[k] = [] }
@@ -72,56 +83,50 @@ module Hecks
72
83
  offenders.empty? || offenders.join("; ")
73
84
  end
74
85
 
75
- # THE FOUNDATIONAL ONE. `Hecks::Runtime` mints nothing — every
86
+ # The foundational one. `Hecks::Runtime` mints nothing — every
76
87
  # identity is declared and derived, never invented (see
77
- # command_interpreter.rb's own "NOTHING IS MINTED" — a random hex,
88
+ # command_interpreter.rb's own "nothing is minted" — a random hex,
78
89
  # a counter, anything not reproducible from the payload, was
79
90
  # 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
91
+ # So the same steps, replayed against a fresh boot, must produce
92
+ # byte-identical events, refusals, and instances — any drift here
82
93
  # is nondeterminism the runtime promised not to have: a wall-clock
83
94
  # read that leaked into compared state, a Hash iteration order a
84
95
  # comparison depended on, anything. Two independent replays, not a
85
- # cached one compared to itself, so a bug that corrupts the FIRST
96
+ # cached one compared to itself, so a bug that corrupts the first
86
97
  # run's own bookkeeping cannot pass by agreeing with itself.
98
+ #
99
+ # @param domain_path [String] path to the domain directory to boot, such as
100
+ # `"examples/pizzas"`
101
+ # @param steps [Array<Hash>] the step list to replay twice
102
+ # @param adapter [Symbol] persistence adapter to boot with (`:memory`,
103
+ # `:postgres`, or `:postgres_era`)
104
+ # @return [true, String] true if both replays produce identical histories
105
+ # (after stripping declared nondeterministic fields); otherwise a message
106
+ # naming the step count that diverged
87
107
  def replay_is_deterministic(domain_path, steps, adapter: :memory)
88
108
  first = Replay.call(domain_path, steps, adapter: adapter)
89
109
  second = Replay.call(domain_path, steps, adapter: adapter)
90
110
 
91
- # `:event_uid`/`:delivery_id` — `Runtime::Outbox::Fanout#rows_for`'s
92
- # OWN `SecureRandom.uuid`, minted fresh per enqueue specifically so
93
- # it stays OFF `Event#to_h` (that file's own comment: the domain's
94
- # own events stay mintless; this is Ruby-only relay bookkeeping,
95
- # never part of what a replay claims the DOMAIN produced) — so two
96
- # otherwise-identical replays legitimately carry two different
97
- # uuids here, the same reason `:bluebook`/`:bluebooks` (live
98
- # object identities, not values) are excluded below.
99
- #
100
- # `row[:event][:occurred_at]` — the outbox row's own `event:` field
101
- # is `Outbox.serialize_event`'s `event.to_h.merge(correlation:...)`,
102
- # the FULL `Event#to_h`, unlike `history[:events]` (this file's own
103
- # `events = runtime.events.map { {name:, aggregate:, id:, payload:}
104
- # }`, above) which has ALWAYS deliberately left `occurred_at` OFF
105
- # the compared surface for exactly this reason: a wall-clock read,
106
- # never reproducible byte-for-byte between two independent
107
- # replaying processes a second apart. Stripped here the same way,
108
- # for the one place it newly reappears.
109
- #
110
- # Both stripped from each `outbox_traces` row before comparing
111
- # rather than dropping `outbox_traces` wholesale: everything else
112
- # on a row (status, consumer, kind, the event's own name/
113
- # aggregate/id/payload) IS reproducible from the steps alone and
111
+ # What leaves this comparison, and why, is declared once in
112
+ # `Nondeterministic::FIELDS`: the `outbox_row` group (per-enqueue
113
+ # uuids), the `event` group on each row's full `Event#to_h`
114
+ # (`occurred_at`, which `history[:events]` never carried), and the
115
+ # `history` group (live IR objects). Stripped per `outbox_traces`
116
+ # row rather than dropping `outbox_traces` wholesale: everything
117
+ # else on a row (status, consumer, kind, the event's own name/
118
+ # aggregate/id/payload) is reproducible from the steps alone and
114
119
  # stays checked.
115
120
  strip_outbox_nondeterminism = lambda do |history|
116
121
  traces = Array(history[:outbox_traces]).map do |trace|
117
122
  trace.merge(rows: trace[:rows].map do |row|
118
- row.except(:event_uid, :delivery_id).merge(event: row[:event].except(:occurred_at))
123
+ Nondeterministic.strip(row, :outbox_row).merge(event: Nondeterministic.strip(row[:event], :event))
119
124
  end)
120
125
  end
121
126
  history.merge(outbox_traces: traces)
122
127
  end
123
128
 
124
- comparable = ->(history) { strip_outbox_nondeterminism.call(history).except(:bluebook, :bluebooks) }
129
+ comparable = ->(history) { Nondeterministic.strip(strip_outbox_nondeterminism.call(history), :history) }
125
130
  return true if comparable.call(first) == comparable.call(second)
126
131
 
127
132
  "two replays of the same #{steps.length} steps produced different histories"
@@ -3,65 +3,73 @@ require_relative "../../bluebook/expression/evaluator"
3
3
  module Hecks
4
4
  module Fuzzing
5
5
  module Properties
6
- # THE TRANSACTIONAL OUTBOX'S OWN CONTRACT (`Runtime::Outbox`, that
6
+ # The transactional outbox's own contract (`Runtime::Outbox`, that
7
7
  # file's own header), held to the history a replay actually produced
8
8
  # rather than trusted. Two facts, each independently checkable from
9
9
  # `history[:outbox_traces]` (`Replay#call`'s own before/after
10
10
  # capture, one entry per step whose dispatch enqueued at least one
11
11
  # row):
12
12
  #
13
- # 1. "DELIVERY IS INLINE BY DEFAULT" — a row THIS replay's own
13
+ # ## The two checks
14
+ #
15
+ # 1. "delivery is inline by default" — a row this replay's own
14
16
  # dispatch enqueued must not still be `pending`/`claimed` once
15
- # that SAME call returns (nothing here ever simulates a
17
+ # that same call returns (nothing here ever simulates a
16
18
  # crash), and must not be `failed` either — `deliver_row`'s
17
19
  # own rescue only reaches `failed` for a genuine defect in the
18
- # RELAY's own consumer resolution (a row naming a policy/
20
+ # relay's own consumer resolution (a row naming a policy/
19
21
  # process_manager `run_consumer`'s own independent registry
20
22
  # lookup cannot find — `WiringError`), never an ordinary
21
23
  # domain refusal (`PolicyInterpreter#deliver`/`SagaInterpreter#
22
- # advance` both rescue those THEMSELVES, recording `delivered:
24
+ # advance` both rescue those themselves, recording `delivered:
23
25
  # false` on the reaction/saga log and letting `run_consumer`
24
26
  # return normally). Both checked for every row, `saga:` and
25
27
  # `policy:` alike.
26
28
  #
27
- # 2. A `policy:` ROW SPECIFICALLY — `PolicyInterpreter#deliver`
29
+ # 2. A `policy:` row specifically — `PolicyInterpreter#deliver`
28
30
  # returns `nil` (no `reaction_log` entry appended at all)
29
- # EXACTLY when its own `where` (or, for a fan-out policy, the
30
- # SAME `where`, gating the whole `for_each`) does not hold;
31
- # every OTHER outcome (delivered, refused, a defect,
31
+ # exactly when its own `where` (or, for a fan-out policy, the
32
+ # same `where`, gating the whole `for_each`) does not hold;
33
+ # every other outcome (delivered, refused, a defect,
32
34
  # reaction-depth-reached) is still a non-nil record `#react`
33
- # appends. So a `delivered` policy row with NO matching
34
- # `reaction_log` entry is legitimate ONLY when that policy's
35
- # own `where`, independently RE-EVALUATED here against the
35
+ # appends. So a `delivered` policy row with no matching
36
+ # `reaction_log` entry is legitimate only when that policy's
37
+ # own `where`, independently re-evaluated here against the
36
38
  # row's own recorded event, genuinely does not hold. A
37
- # `for_each` policy's own fan-out correctness (how MANY rows
39
+ # `for_each` policy's own fan-out correctness (how many rows
38
40
  # it should have dispatched to) is `fanout_dispatches_once_
39
41
  # per_matching_row`'s job, not this one's.
40
42
  #
41
- # A `saga:` ROW HAS NO EQUIVALENT SECOND CHECK, DELIBERATELY — this
42
- # was the first shape this property shipped with, and it was WRONG,
43
- # caught live against `examples/banking` before this comment
44
- # existed: `Fanout.sagas`' own `listens?` (starts_on/ends_on/
45
- # handler_for matching the event NAME alone) says nothing about
46
- # whether a CORRELATION resolves or a LIVE INSTANCE exists, and
43
+ # ## Why a `saga:` row is exempt from check 2
44
+ #
45
+ # Deliberately: `Fanout.sagas`' own `listens?` (starts_on/ends_on/
46
+ # handler_for matching the event name alone) says nothing about
47
+ # whether a correlation resolves or a live instance exists, and
47
48
  # `begin_saga`/`end_saga` (saga_interpreter.rb) both have silent,
48
- # perfectly ordinary no-op paths that append NOTHING to `saga_log`
49
+ # perfectly ordinary no-op paths that append nothing to `saga_log`
49
50
  # — `begin_saga` when an instance under that correlation already
50
- # exists, `end_saga` when NO live instance exists to end (an
51
+ # exists, `end_saga` when no live instance exists to end (an
51
52
  # `AccountOpened` fired by opening an account directly, bypassing
52
53
  # the onboarding flow whose `ends_on` names that same event,
53
54
  # reproduces this exactly: `Fanout.listens?` enqueues the row
54
- # because the event NAME matches `ends_on`, `end_saga` finds
55
+ # because the event name matches `ends_on`, `end_saga` finds
55
56
  # nothing under that correlation to delete, and neither logs a
56
57
  # word). A `saga:` row draining to `delivered` with zero matching
57
- # `saga_log` entries is therefore NOT a finding — only check 1
58
+ # `saga_log` entries is therefore not a finding — only check 1
58
59
  # applies to it.
59
60
  #
60
- # NOT A GRAMMAR CONSTRUCT — `FEATURE_COVERAGE`'s own `dry_runs_
61
- # leave_no_trace` precedent: the outbox is a runtime door
62
- # (`Runtime::Outbox`), not a word a bluebook declares, so there is
63
- # no feature string here to claim.
61
+ # ## Not a grammar construct
62
+ #
63
+ # `FEATURE_COVERAGE`'s own `dry_runs_leave_no_trace` precedent: the
64
+ # outbox is a runtime door (`Runtime::Outbox`), not a word a
65
+ # bluebook declares, so there is no feature string here to claim.
64
66
  module Outbox
67
+ # Checks every outbox row a replay's history recorded against the
68
+ # outbox's own contract (see this file's own header).
69
+ #
70
+ # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
71
+ # @return [true, String] true if every outbox row satisfies the contract; otherwise
72
+ # a semicolon-joined message naming each offending row
65
73
  def outbox_rows_match_reactions(history)
66
74
  bluebooks = history.fetch(:bluebooks, {})
67
75
 
@@ -72,6 +80,17 @@ module Hecks
72
80
  offenders.empty? || offenders.join("; ")
73
81
  end
74
82
 
83
+ # Checks one outbox row against the outbox's own contract.
84
+ #
85
+ # @param row [Hash] one `Runtime::Outbox::Row#to_h` entry, `trace[:rows]`'s shape:
86
+ # at least `:status`, `:delivery_id`, `:consumer`, `:event`, and (when failed)
87
+ # `:error`
88
+ # @param trace [Hash] this row's outbox trace entry, one of `history[:outbox_traces]`,
89
+ # carrying `:rows` and `:reactions`
90
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
91
+ # keyed by domain name
92
+ # @return [Array<String>] zero or one offending message naming what this row got
93
+ # wrong; empty when the row is fine or its status names no check
75
94
  def outbox_row_offenders(row, trace, bluebooks)
76
95
  on = row.dig(:event, :name)
77
96
 
@@ -90,9 +109,20 @@ module Hecks
90
109
  end
91
110
  end
92
111
 
112
+ # Checks one delivered `policy:` outbox row for a missing `reaction_log` entry.
113
+ #
93
114
  # See this file's own header for why a `saga:` row is exempt: its
94
115
  # own `listens?` gives no such guarantee, unlike a policy's single,
95
116
  # deterministic `where` gate.
117
+ #
118
+ # @param row [Hash] the delivered outbox row being checked
119
+ # @param on [String] the row's event name, `row.dig(:event, :name)`
120
+ # @param trace [Hash] this row's outbox trace entry, carrying `:reactions`
121
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
122
+ # keyed by domain name
123
+ # @return [Array<String>] zero or one offending message; empty when the row is
124
+ # not a policy row, the policy is undeclared or fans out, or the policy's
125
+ # `where` does not independently re-evaluate true
96
126
  def outbox_delivered_policy_offenders(row, on, trace, bluebooks)
97
127
  kind, fqn = row[:consumer].to_s.split(":", 2)
98
128
  return [] unless kind == "policy"
@@ -101,11 +131,14 @@ module Hecks
101
131
  return [] if trace[:reactions].any? { |entry| entry[:policy] == name && entry[:on] == on }
102
132
 
103
133
  policy = bluebooks[home]&.policies&.find { |candidate| candidate.name == name }
104
- return [] unless policy # nothing declared under this name — inconclusive, not a claimed mismatch
105
- return [] if policy.fans_out? # fan-out row count is fanout_dispatches_once_per_matching_row's job
134
+ # Nothing declared under this name — inconclusive, not a claimed mismatch.
135
+ return [] unless policy
136
+ # Fan-out row count is fanout_dispatches_once_per_matching_row's job.
137
+ return [] if policy.fans_out?
106
138
 
107
139
  held = independently_re_evaluate_policy_where(policy, row[:event])
108
- return [] if held != true # false, or inconclusive (the where itself raised) — never a claimed mismatch
140
+ # False, or inconclusive (the where itself raised) — never a claimed mismatch.
141
+ return [] if held != true
109
142
 
110
143
  ["outbox row #{row[:delivery_id]} (#{row[:consumer]} on #{on}) drained as delivered, but no matching " \
111
144
  "reaction_log entry exists and the policy's own where clause independently re-evaluates true — " \
@@ -117,17 +150,22 @@ module Hecks
117
150
  # with itself (the same rule `resolve_dispatch_binding`'s own
118
151
  # comment states). `Evaluator.call` (the raw-string entry, parsed
119
152
  # and cached — never `call_rule`, which needs the policy's own
120
- # BUILD-TIME `where_rule` AST, an object this history has no
153
+ # build-time `where_rule` AST, an object this history has no
121
154
  # reason to carry) is the exact same call `Replay#fan_out_finding`
122
155
  # already makes for the identical fact one property over
123
156
  # (`policy.where.to_s.empty? || Evaluator.call(policy.where, {},
124
157
  # payload)`), reused rather than re-derived a second, slightly
125
158
  # different way. `rescue`d to `nil`, not `false`: a where clause
126
159
  # that cannot be re-evaluated from the row's own recorded payload
127
- # alone is INCONCLUSIVE, not proof either way — the same "never a
160
+ # alone is inconclusive, not proof either way — the same "never a
128
161
  # claimed pass or a claimed mismatch from a resolution this replay
129
162
  # cannot actually reproduce" discipline `build_guard_check`'s own
130
163
  # rescue clause already follows.
164
+ #
165
+ # @param policy [Bluebook::Policy] the policy whose `where` clause is re-evaluated
166
+ # @param event [Hash] the outbox row's own recorded event, read for `:payload`
167
+ # @return [Boolean, nil] whether `policy`'s `where` holds against `event`'s payload,
168
+ # or nil if the where clause cannot be re-evaluated from it
131
169
  def independently_re_evaluate_policy_where(policy, event)
132
170
  return true if policy.where.to_s.empty?
133
171
 
@@ -10,7 +10,7 @@ module Hecks
10
10
  # (#query_for_verb, #query_eligible_rows, #resolve_hop_clause) other
11
11
  # property modules in this directory also call.
12
12
  module Querying
13
- # THE QUERY ORACLE — differential testing within the one runtime,
13
+ # The query oracle — differential testing within the one runtime,
14
14
  # the shape the retired cross-runtime harness should always have
15
15
  # been. Every generated ask was answered twice at the same instant
16
16
  # (Replay records both): once through whatever the aggregate is
@@ -18,23 +18,27 @@ module Hecks
18
18
  # a SQL binding would compile it), once through the reference
19
19
  # interpreter's own evaluation. The two are separate, live
20
20
  # implementations of the same comparator vocabulary, and they have
21
- # drifted before — an adapter that ACCEPTS what the reference says
21
+ # drifted before — an adapter that accepts what the reference says
22
22
  # matches nothing, or orders what it refuses to order, shows up
23
23
  # here as a finding no self-referential adapter spec could see.
24
- # M23 — `Replay` now runs the native and reference engines
25
- # INDEPENDENTLY (each in its own begin/rescue — see that file's own
26
- # comment at the capture site), so this property can tell apart what
27
- # used to be indistinguishable: "both engines refused" (fine — the
28
- # ask was genuinely bad, nothing to compare) from "one refused and
29
- # the other did not" (a real divergence — the two engines disagree
30
- # about whether the ask was even VALID, never mind what it answers).
31
- # `native_refused`/`reference_refused` are read by KEY PRESENCE, not
32
- # truthiness — `Replay` only ever adds `:error`/`:reference_error`
33
- # to an entry when that side actually raised, so an absent key is an
34
- # unambiguous "this side answered." A read-model ask (no reference
35
- # twin attempted at all, `asked[:query]` without "::") is skipped
36
- # entirely, same as always — there is no second engine to disagree
37
- # with.
24
+ # M23 — `Replay` runs the native and reference engines independently
25
+ # (each in its own begin/rescue — see that file's own comment at the
26
+ # capture site), so this property can tell apart "both engines
27
+ # refused" (fine — the ask was genuinely bad, nothing to compare)
28
+ # from "one refused and the other did not" (a real divergence — the
29
+ # two engines disagree about whether the ask was even valid, never
30
+ # mind what it answers). `native_refused`/`reference_refused` are
31
+ # read by key presence, not truthiness — `Replay` only ever adds
32
+ # `:error`/`:reference_error` to an entry when that side actually
33
+ # raised, so an absent key is an unambiguous "this side answered." A
34
+ # read-model ask (no reference twin attempted at all, `asked[:query]`
35
+ # without "::") is skipped entirely, same as always — there is no
36
+ # second engine to disagree with.
37
+ #
38
+ # @param history [Hash] a replayed history as returned by `Replay.call`
39
+ # @return [true, String] true if every native/reference query answer, and every
40
+ # refusal of one, agrees; otherwise a message naming the offending query, its
41
+ # args, and how the two engines disagreed
38
42
  def query_answers_match_reference(history)
39
43
  offenders = history.fetch(:queries).filter_map do |asked|
40
44
  next unless asked[:query].is_a?(String) && asked[:query].include?("::")
@@ -59,16 +63,16 @@ module Hecks
59
63
  offenders.empty? || offenders.join("; ")
60
64
  end
61
65
 
62
- # THE SAME "TWO ENGINES, COMPARED" SHAPE query_answers_match_reference
66
+ # The same "two engines, compared" shape query_answers_match_reference
63
67
  # already uses, aimed squarely at Query#options' offset/limit pair —
64
- # but recomputed from history[:instances] directly, a THIRD,
68
+ # but recomputed from history[:instances] directly, a third,
65
69
  # independent computation, rather than comparing QueryInterpreter's
66
70
  # own native and reference paths against each other (which could
67
71
  # share the identical bug neither implementation happened to hit —
68
- # see #4's own fix, which touched BOTH #interpret and
72
+ # see #4's own fix, which touched both #interpret and
69
73
  # #reference_interpret at once). `order_by` declared alongside
70
74
  # `offset` or `limit` names a genuinely paged query. Ports::Query::
71
- # Ordering.apply is the SAME engine QueryInterpreter#ordered calls,
75
+ # Ordering.apply is the same engine QueryInterpreter#ordered calls,
72
76
  # reused here rather than re-derived, so this oracle cannot drift
73
77
  # from what "in order" means without the interpreter drifting the
74
78
  # identical way — only the offset-then-limit .drop/.first slice
@@ -83,6 +87,11 @@ module Hecks
83
87
  # `skipped`/`expected` across method boundaries as params/returns
84
88
  # for a sequence that's only ever computed once, in this order.
85
89
  # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
90
+ #
91
+ # @param history [Hash] a replayed history as returned by `Replay.call`
92
+ # @return [true, String] true if every paged query's real answer matches an
93
+ # independent order/offset/limit recomputation over its eligible rows;
94
+ # otherwise a message naming the query, its args, and the two answers
86
95
  def paging_offset_partitions_correctly(history)
87
96
  bluebooks = history.fetch(:bluebooks)
88
97
 
@@ -112,13 +121,20 @@ module Hecks
112
121
  offenders.empty? || offenders.join("; ")
113
122
  end
114
123
 
115
- # THE DECLARED Query ITSELF, resolved from a replayed verb — the
124
+ # The declared Query itself, resolved from a replayed verb — the
116
125
  # same shape #command_for_verb resolves a command by, one
117
126
  # construct over. Entity-level queries (a dotted query_path) are
118
127
  # out of scope here — paging on an entity's own list has no real
119
128
  # corpus site yet, and the "one many-side head, one aggregate,
120
129
  # no FK-join" shape #query_eligible_rows assumes doesn't hold for
121
130
  # one.
131
+ #
132
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain,
133
+ # keyed by domain name
134
+ # @param verb [String] the asked verb, `"Domain::Aggregate.Query"`
135
+ # @return [Bluebook::Query, nil] the declared query, or nil if `verb` names an
136
+ # entity-level query path, or a domain, aggregate, or query not found among
137
+ # `bluebooks`
122
138
  def query_for_verb(bluebooks, verb)
123
139
  domain, aggregate_name, query_path = Naming.split_verb(verb)
124
140
  return nil unless query_path && !query_path.include?(".")
@@ -128,27 +144,42 @@ module Hecks
128
144
  aggregate&.query(query_path)
129
145
  end
130
146
 
131
- # A QUERY'S OWN ROWS — unlike #eligible_rows (a ReadModel's
147
+ # Independently recomputes which of an aggregate's own stored rows a
148
+ # query's `wheres` admit.
149
+ #
150
+ # A query's own rows — unlike #eligible_rows (a ReadModel's
132
151
  # reduced/grouped many-side head, possibly FK-joined against a
133
- # root), a Query always asks about its OWN owning aggregate
152
+ # root), a Query always asks about its own owning aggregate
134
153
  # directly ; no join, no reference_target. `id:` merged in the
135
154
  # same way #eligible_rows' own rows are, since a stable sort
136
155
  # (Ordering.apply's own `identity:`) and the real answer's own
137
156
  # `record.state.merge(id: record.id)` both need it.
138
- # `bluebooks:` — needed ONLY to recognise and resolve a `/` HOP
157
+ # `bluebooks:` — needed only to recognise and resolve a `/` hop
139
158
  # clause (`engagement/client/status`, hop_chain.bluebook's own
140
- # PricedAboveViaEngagement): a hop's head names one of the OWNING
159
+ # PricedAboveViaEngagement): a hop's head names one of the owning
141
160
  # aggregate's declared references, and only the declaration graph
142
161
  # can say which attribute that is and which aggregate it targets.
143
162
  # A local clause never consults it. Latent gap this closed, found
144
163
  # by the fuzzer itself the first time a generated sequence ever
145
- # built a full hop chain AND had its paged query answer a row
164
+ # built a full hop chain and had its paged query answer a row
146
165
  # (seed 1, the moment scalar_value_objects.bluebook joined the
147
166
  # fixtures corpus and shifted every seeded draw): the recompute
148
- # dug `engagement/client/status` as a LOCAL dotted path, found
167
+ # dug `engagement/client/status` as a local dotted path, found
149
168
  # nil, and declared every genuinely-eligible row ineligible — a
150
169
  # false property violation against a correct runtime answer,
151
170
  # reproducible on an untouched main with this same 4-step script.
171
+ #
172
+ # @param instances [Hash{String => Hash}] `history[:instances]`, or an
173
+ # `:instances_at` snapshot from a replayed query entry
174
+ # @param domain [String] the domain name the target aggregate belongs to
175
+ # @param aggregate_name [String] the target aggregate's own declared name
176
+ # @param wheres [Array<QuerySpecification::Common::WhereClause>] the clauses
177
+ # every returned row must satisfy
178
+ # @param args [Hash] the query's own call args, for a clause whose value is a
179
+ # Symbol naming one
180
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain, keyed
181
+ # by domain name; needed only to resolve a `/` hop clause
182
+ # @return [Array<Hash>] each admitted row's own state, merged with its `id:`
152
183
  def query_eligible_rows(instances, domain, aggregate_name, wheres, args, bluebooks: {})
153
184
  aggregate = bluebooks[domain]&.aggregate(aggregate_name)
154
185
  prefix = "#{domain}::#{aggregate_name}#"
@@ -166,6 +197,9 @@ module Hecks
166
197
  end
167
198
  end
168
199
 
200
+ # Resolves one hop of a `/`-chained clause into a local `in` clause against
201
+ # the target aggregate's own ids.
202
+ #
169
203
  # `Runtime::ReferenceHop#fold`, independently restated over the
170
204
  # replay's own `:instances_at` snapshot instead of live
171
205
  # repositories — the same shape every other recompute in this
@@ -173,13 +207,27 @@ module Hecks
173
207
  # would be checking the runtime against itself). One hop peels
174
208
  # off the head (`HopPath.next_hop`, the identical one-step
175
209
  # primitive the live fold uses), the inner clause recurses
176
- # through `query_eligible_rows` against the TARGET's own
210
+ # through `query_eligible_rows` against the target's own
177
211
  # snapshot rows (so a multi-hop tail resolves hop by hop, exactly
178
212
  # as the live path's own recursion does), and the ids that
179
213
  # answered fold back as the same local `in` membership clause the
180
214
  # live fold builds. A clause with no `/`, or one whose head this
181
215
  # aggregate's declarations cannot resolve, passes through
182
216
  # untouched and evaluates locally as it always did.
217
+ #
218
+ # @param instances [Hash{String => Hash}] `history[:instances]`, or an
219
+ # `:instances_at` snapshot from a replayed query entry
220
+ # @param domain [String] the domain name `aggregate` belongs to
221
+ # @param aggregate [Bluebook::Aggregate, nil] the aggregate `clause` is
222
+ # evaluated against; nil skips hop resolution entirely
223
+ # @param clause [QuerySpecification::Common::WhereClause] the clause to resolve
224
+ # @param args [Hash] the query's own call args, passed through to the inner
225
+ # hop's own `query_eligible_rows` recursion
226
+ # @param bluebooks [Hash{String => Bluebook::Chapter}] every loaded domain, keyed
227
+ # by domain name
228
+ # @return [QuerySpecification::Common::WhereClause] `clause` unchanged if it has
229
+ # no resolvable hop head; otherwise a local `in` clause over the matching
230
+ # target ids
183
231
  def resolve_hop_clause(instances, domain, aggregate, clause, args, bluebooks)
184
232
  return clause unless aggregate && QuerySpecification::HopPath.hop_head?(clause.field, aggregate.attributes)
185
233
 
@@ -194,9 +242,15 @@ module Hecks
194
242
  QuerySpecification::Common::WhereClause.new(field: hop.attribute.name, op: "in", value: ids)
195
243
  end
196
244
 
245
+ # Resolves a declared limit/offset value against the query's own call args.
246
+ #
197
247
  # `QueryInterpreter#resolve_query_value`, reproduced: a declared
198
248
  # limit/offset is either a literal or a Symbol naming an argument
199
249
  # the caller supplied.
250
+ #
251
+ # @param value [Integer, Symbol] the declared limit/offset value
252
+ # @param args [Hash] the query's own call args
253
+ # @return [Object] `value` unchanged if not a Symbol; otherwise `args[value]`
200
254
  def resolve_paging_value(value, args)
201
255
  value.is_a?(Symbol) ? args[value] : value
202
256
  end