hecks 0.2.0 → 0.3.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 (408) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +403 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +62 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +148 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +131 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +80 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +391 -0
  31. data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
  32. data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
  33. data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
  34. data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
  35. data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
  36. data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
  37. data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
  38. data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
  39. data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
  40. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
  41. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
  42. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  43. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
  44. data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
  45. data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
  46. data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
  47. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  48. data/lib/hecks/adapters/driven/prism.rb +80 -0
  49. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  50. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  51. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  52. data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
  53. data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
  54. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
  55. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  56. data/lib/hecks/adapters/driven/sqlite.rb +285 -0
  57. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  58. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  59. data/lib/hecks/adapters/driven.rb +36 -0
  60. data/lib/hecks/adapters.rb +6 -0
  61. data/lib/hecks/behaviors/dsl.rb +96 -0
  62. data/lib/hecks/behaviors/expectations.rb +280 -0
  63. data/lib/hecks/behaviors/ir.rb +31 -0
  64. data/lib/hecks/behaviors/rspec.rb +42 -0
  65. data/lib/hecks/behaviors/runner.rb +96 -0
  66. data/lib/hecks/behaviors.rb +25 -0
  67. data/lib/hecks/bluebook/aggregate.rb +108 -0
  68. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  69. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  70. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  71. data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
  72. data/lib/hecks/bluebook/assembly/marks.rb +227 -0
  73. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  74. data/lib/hecks/bluebook/assembly.rb +78 -0
  75. data/lib/hecks/bluebook/attribute.rb +96 -0
  76. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  77. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  78. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  79. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  80. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  81. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  82. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  83. data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
  84. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  85. data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
  86. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  87. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  88. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  89. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  90. data/lib/hecks/bluebook/chapter.rb +68 -0
  91. data/lib/hecks/bluebook/command.rb +124 -0
  92. data/lib/hecks/bluebook/domain_port.rb +102 -0
  93. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  94. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
  95. data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
  96. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  97. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
  98. data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
  99. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  100. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  101. data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
  102. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  103. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  104. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  105. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  106. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  107. data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
  108. data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
  109. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  110. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
  111. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  112. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  113. data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
  114. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  115. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  116. data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
  117. data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
  118. data/lib/hecks/bluebook/dsl.rb +45 -0
  119. data/lib/hecks/bluebook/entity.rb +103 -0
  120. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
  121. data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
  122. data/lib/hecks/bluebook/expression/projection.json +170 -0
  123. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  124. data/lib/hecks/bluebook/expression/resolver.rb +630 -0
  125. data/lib/hecks/bluebook/expression.rb +13 -0
  126. data/lib/hecks/bluebook/hexagon.rb +60 -0
  127. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  128. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  129. data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
  130. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  131. data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
  132. data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
  133. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
  134. data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
  135. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  136. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  137. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  138. data/lib/hecks/bluebook/meta_validator.rb +452 -0
  139. data/lib/hecks/bluebook/model_check.rb +318 -0
  140. data/lib/hecks/bluebook/pattern_subset.rb +120 -0
  141. data/lib/hecks/bluebook/policy.rb +41 -0
  142. data/lib/hecks/bluebook/process_manager.rb +86 -0
  143. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  144. data/lib/hecks/bluebook/project_loader.rb +40 -0
  145. data/lib/hecks/bluebook/project_register.rb +82 -0
  146. data/lib/hecks/bluebook/query.rb +61 -0
  147. data/lib/hecks/bluebook/read_model.rb +109 -0
  148. data/lib/hecks/bluebook/reference.rb +74 -0
  149. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  150. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  151. data/lib/hecks/bluebook/translation.rb +92 -0
  152. data/lib/hecks/bluebook/value_object.rb +58 -0
  153. data/lib/hecks/bluebook.rb +74 -0
  154. data/lib/hecks/codemod.rb +342 -0
  155. data/lib/hecks/construct.rb +71 -0
  156. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  157. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  158. data/lib/hecks/deploy/oidc.json +18 -0
  159. data/lib/hecks/doc/reference.rb +389 -0
  160. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  161. data/lib/hecks/facade/cli_door.rb +116 -0
  162. data/lib/hecks/facade/cli_runner.rb +190 -0
  163. data/lib/hecks/facade/command_request.rb +105 -0
  164. data/lib/hecks/facade/handle.rb +173 -0
  165. data/lib/hecks/facade/json_door.rb +154 -0
  166. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  167. data/lib/hecks/facade/surface/chapter.rb +107 -0
  168. data/lib/hecks/facade/surface.rb +48 -0
  169. data/lib/hecks/facade.rb +44 -0
  170. data/lib/hecks/forms/app.rb +300 -0
  171. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  172. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  173. data/lib/hecks/forms/field_renderer.rb +170 -0
  174. data/lib/hecks/forms/field_shape.rb +232 -0
  175. data/lib/hecks/forms/html.rb +53 -0
  176. data/lib/hecks/forms/index_renderer.rb +35 -0
  177. data/lib/hecks/forms/page.rb +157 -0
  178. data/lib/hecks/forms/params.rb +132 -0
  179. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  180. data/lib/hecks/forms/record_renderer.rb +115 -0
  181. data/lib/hecks/forms/record_table.rb +63 -0
  182. data/lib/hecks/forms/reference_options.rb +30 -0
  183. data/lib/hecks/forms/value_object_shape.rb +46 -0
  184. data/lib/hecks/forms.rb +54 -0
  185. data/lib/hecks/fqn.rb +94 -0
  186. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  187. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  188. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  189. data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
  190. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  191. data/lib/hecks/framework/oidc.json +39 -0
  192. data/lib/hecks/framework.rb +90 -0
  193. data/lib/hecks/freezer.rb +67 -0
  194. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  195. data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
  196. data/lib/hecks/fuzzing/properties.rb +1146 -0
  197. data/lib/hecks/fuzzing/replay.rb +635 -0
  198. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  199. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
  200. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  201. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
  202. data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
  203. data/lib/hecks/fuzzing/value_generator.rb +177 -0
  204. data/lib/hecks/fuzzing.rb +12 -0
  205. data/lib/hecks/grammar/evolve.rb +291 -0
  206. data/lib/hecks/grammar/expression.bluebook +417 -0
  207. data/lib/hecks/grammar/expression_operators.json +1152 -0
  208. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  209. data/lib/hecks/grammar/oidc.json +69 -0
  210. data/lib/hecks/grammar/translation.bluebook +203 -0
  211. data/lib/hecks/grammar.rb +178 -0
  212. data/lib/hecks/ir.rb +126 -0
  213. data/lib/hecks/language/adapter.bluebook +116 -0
  214. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  215. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  216. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  217. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  218. data/lib/hecks/language/bluebook/command.bluebook +437 -0
  219. data/lib/hecks/language/bluebook/entity.bluebook +316 -0
  220. data/lib/hecks/language/bluebook/policy.bluebook +172 -0
  221. data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
  222. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  223. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  224. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  225. data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
  226. data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
  227. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  228. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  229. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  230. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  231. data/lib/hecks/language/oidc.json +313 -0
  232. data/lib/hecks/language/port.bluebook +92 -0
  233. data/lib/hecks/language/translation/translation.bluebook +110 -0
  234. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  235. data/lib/hecks/language/world/wiring.bluebook +62 -0
  236. data/lib/hecks/language/world/world.bluebook +84 -0
  237. data/lib/hecks/literal.rb +125 -0
  238. data/lib/hecks/naming.rb +121 -0
  239. data/lib/hecks/ports/access_control.port +4 -0
  240. data/lib/hecks/ports/access_control.rb +62 -0
  241. data/lib/hecks/ports/agent/answers.rb +104 -0
  242. data/lib/hecks/ports/agent.port +4 -0
  243. data/lib/hecks/ports/agent.rb +167 -0
  244. data/lib/hecks/ports/authentication.port +4 -0
  245. data/lib/hecks/ports/authentication.rb +50 -0
  246. data/lib/hecks/ports/authorization.port +4 -0
  247. data/lib/hecks/ports/authorization.rb +59 -0
  248. data/lib/hecks/ports/clock.port +4 -0
  249. data/lib/hecks/ports/clock.rb +62 -0
  250. data/lib/hecks/ports/extraction.port +4 -0
  251. data/lib/hecks/ports/extraction.rb +37 -0
  252. data/lib/hecks/ports/identity_assignment.port +4 -0
  253. data/lib/hecks/ports/identity_assignment.rb +45 -0
  254. data/lib/hecks/ports/identity_generation.port +4 -0
  255. data/lib/hecks/ports/identity_generation.rb +49 -0
  256. data/lib/hecks/ports/identity_resolution.port +4 -0
  257. data/lib/hecks/ports/identity_resolution.rb +40 -0
  258. data/lib/hecks/ports/loading.port +4 -0
  259. data/lib/hecks/ports/loading.rb +13 -0
  260. data/lib/hecks/ports/persistence/append_only.rb +102 -0
  261. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  262. data/lib/hecks/ports/persistence/execution.rb +18 -0
  263. data/lib/hecks/ports/persistence/lineage.rb +277 -0
  264. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  266. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  267. data/lib/hecks/ports/persistence.port +4 -0
  268. data/lib/hecks/ports/persistence.rb +39 -0
  269. data/lib/hecks/ports/projection.port +4 -0
  270. data/lib/hecks/ports/projection.rb +76 -0
  271. data/lib/hecks/ports/query/in_memory.rb +60 -0
  272. data/lib/hecks/ports/query/ordering.rb +41 -0
  273. data/lib/hecks/ports/query.rb +36 -0
  274. data/lib/hecks/ports.rb +27 -0
  275. data/lib/hecks/projections/diagrams.rb +413 -0
  276. data/lib/hecks/projections/ir.rb +18 -0
  277. data/lib/hecks/projections/model/deviations.rb +98 -0
  278. data/lib/hecks/projections/model.rb +145 -0
  279. data/lib/hecks/projections/oidc.rb +110 -0
  280. data/lib/hecks/projections/parser_table.rb +159 -0
  281. data/lib/hecks/projections/reference.rb +38 -0
  282. data/lib/hecks/projections/shape.rb +31 -0
  283. data/lib/hecks/projections/statements.rb +110 -0
  284. data/lib/hecks/projections/vocabulary.rb +100 -0
  285. data/lib/hecks/projections.rb +32 -0
  286. data/lib/hecks/projector/cli_projector.rb +437 -0
  287. data/lib/hecks/projector/docs_projector.rb +321 -0
  288. data/lib/hecks/projector/exporter.rb +141 -0
  289. data/lib/hecks/projector/ir_projector.rb +18 -0
  290. data/lib/hecks/projector/narrate_projector.rb +243 -0
  291. data/lib/hecks/projector/target.rb +97 -0
  292. data/lib/hecks/projector.rb +186 -0
  293. data/lib/hecks/query_ir.rb +392 -0
  294. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  295. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  296. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  297. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  298. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  299. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  300. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  301. data/lib/hecks/query_specification/common/null_policy.rb +80 -0
  302. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  303. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  304. data/lib/hecks/query_specification/common/options.rb +32 -0
  305. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  306. data/lib/hecks/query_specification/common/specification.rb +10 -0
  307. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  308. data/lib/hecks/query_specification/field_path.rb +89 -0
  309. data/lib/hecks/query_specification/hop_path.rb +130 -0
  310. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  311. data/lib/hecks/query_specification.rb +14 -0
  312. data/lib/hecks/rendering.rb +48 -0
  313. data/lib/hecks/router/namespace_installer.rb +157 -0
  314. data/lib/hecks/router.rb +70 -0
  315. data/lib/hecks/runtime/caller.rb +50 -0
  316. data/lib/hecks/runtime/capability_graph.rb +44 -0
  317. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
  318. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
  319. data/lib/hecks/runtime/command_interpreter.rb +413 -0
  320. data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
  321. data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
  322. data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
  323. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  324. data/lib/hecks/runtime/command_rules/references.rb +181 -0
  325. data/lib/hecks/runtime/command_rules.rb +28 -0
  326. data/lib/hecks/runtime/dependency_planning.rb +231 -0
  327. data/lib/hecks/runtime/dispatcher.rb +286 -0
  328. data/lib/hecks/runtime/entity_element.rb +253 -0
  329. data/lib/hecks/runtime/entity_interpreter.rb +228 -0
  330. data/lib/hecks/runtime/era_check.rb +136 -0
  331. data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
  332. data/lib/hecks/runtime/era_guard.rb +193 -0
  333. data/lib/hecks/runtime/era_tamper.rb +61 -0
  334. data/lib/hecks/runtime/errors.rb +100 -0
  335. data/lib/hecks/runtime/event.rb +51 -0
  336. data/lib/hecks/runtime/identity.rb +128 -0
  337. data/lib/hecks/runtime/instance.rb +120 -0
  338. data/lib/hecks/runtime/interpreting.rb +69 -0
  339. data/lib/hecks/runtime/loader.rb +143 -0
  340. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  341. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  342. data/lib/hecks/runtime/query_interpreter.rb +254 -0
  343. data/lib/hecks/runtime/reaction_invocation.rb +245 -0
  344. data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
  345. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  346. data/lib/hecks/runtime/reference_hop.rb +99 -0
  347. data/lib/hecks/runtime/refusal_wording.rb +124 -0
  348. data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
  349. data/lib/hecks/runtime/registry/verification.rb +158 -0
  350. data/lib/hecks/runtime/registry.rb +218 -0
  351. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  352. data/lib/hecks/runtime/routing.rb +96 -0
  353. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  354. data/lib/hecks/runtime/saga_interpreter.rb +301 -0
  355. data/lib/hecks/runtime/storage_shape.rb +120 -0
  356. data/lib/hecks/runtime/tenant_check.rb +84 -0
  357. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  358. data/lib/hecks/runtime/value/admission.rb +112 -0
  359. data/lib/hecks/runtime/value/coercion.rb +500 -0
  360. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  361. data/lib/hecks/runtime/value.rb +125 -0
  362. data/lib/hecks/runtime.rb +95 -0
  363. data/lib/hecks/translation/audit/approval_digest.rb +31 -0
  364. data/lib/hecks/translation/audit/layer_one.rb +37 -0
  365. data/lib/hecks/translation/audit/layer_two.rb +77 -0
  366. data/lib/hecks/translation/audit/unfed_report.rb +42 -0
  367. data/lib/hecks/translation/audit.rb +70 -0
  368. data/lib/hecks/translation/reattest.rb +72 -0
  369. data/lib/hecks/translation/rule_compiler.rb +120 -0
  370. data/lib/hecks/translation/scaffold/differ.rb +183 -0
  371. data/lib/hecks/translation/scaffold/renderer.rb +41 -0
  372. data/lib/hecks/translation/scaffold/writer.rb +28 -0
  373. data/lib/hecks/translation/scaffold.rb +30 -0
  374. data/lib/hecks/translation.rb +11 -0
  375. data/lib/hecks/version.rb +16 -0
  376. data/lib/hecks/vocabulary.rb +213 -0
  377. data/lib/hecks.rb +118 -6
  378. metadata +401 -222
  379. data/bin/hecks +0 -7
  380. data/bin/hecks-package +0 -65
  381. data/bin/hecks_console +0 -12
  382. data/bin/hecks_serverless +0 -6
  383. data/lib/cli/build.rb +0 -14
  384. data/lib/cli/command_runner.rb +0 -28
  385. data/lib/cli/console.rb +0 -10
  386. data/lib/cli/generate.rb +0 -37
  387. data/lib/cli/hecks-cli.rb +0 -27
  388. data/lib/cli/test.rb +0 -57
  389. data/lib/console/commands.rb +0 -8
  390. data/lib/console/hecks-console.rb +0 -1
  391. data/lib/packager/README.md +0 -0
  392. data/lib/packager/app_runner.rb +0 -21
  393. data/lib/packager/args.rb +0 -26
  394. data/lib/packager/compatibility/fixnum.rb +0 -6
  395. data/lib/packager/hecks.rb +0 -39
  396. data/lib/packager/query_runner.rb +0 -21
  397. data/lib/packager/resources/Dockerfile +0 -11
  398. data/lib/packager/resources/app_binary +0 -7
  399. data/lib/packager/resources/bundle_config +0 -3
  400. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  401. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  402. data/lib/serverless/Domain +0 -32
  403. data/lib/serverless/cli.rb +0 -75
  404. data/lib/serverless/resources/command_name.js +0 -5
  405. data/lib/serverless/resources/environment.js +0 -7
  406. data/lib/serverless/resources/handler.js.tt +0 -28
  407. data/lib/serverless/resources/run_binary.js +0 -22
  408. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,1146 @@
1
+ require "json"
2
+ require_relative "../bluebook/model_check"
3
+ require_relative "../bluebook/meta_validator"
4
+ require_relative "../ports/query/in_memory"
5
+ require_relative "../query_specification/field_path"
6
+ require_relative "../runtime/value"
7
+
8
+ module Hecks
9
+ module Fuzzing
10
+ # Declared properties, checked over a REPLAYED history — the other
11
+ # half of property-based testing the fuzzer was missing: it already
12
+ # generates and (with bin/fuzz's shrinker) minimizes, but checked
13
+ # nothing beyond "did the interpreter crash" and "did the replay
14
+ # match the claim." A property here is a fact that should hold of
15
+ # ANY history a valid domain produces, independent of which seed
16
+ # produced it.
17
+ #
18
+ # Each property is `name => ->(history) { true/false, or a message
19
+ # string naming what broke }` — a truthy return (including `true`)
20
+ # is a pass; a String return is a failure, and the string IS the
21
+ # finding. `history` is Replay's return shape.
22
+ #
23
+ # EVERY PROPERTY DECLARES THE LANGUAGE FEATURE IT COVERS, in
24
+ # `FEATURE_COVERAGE` below — a "Construct#attribute" pair spelled
25
+ # exactly as `Bluebook::MetaValidator.grammar_registry` names it,
26
+ # the SAME meta-domain that judges every real bluebook (see that
27
+ # module's own header: "the language IS the source"). That is the
28
+ # link this file exists to make real: a construct the language
29
+ # declares is a fact `spec/meta_domain_coverage_spec.rb` can
30
+ # enumerate on its own, without anyone re-typing the list here —
31
+ # so a new attribute added to `language/bluebook/*.bluebook` shows
32
+ # up in that spec as UNCLAIMED the moment it lands, not whenever
33
+ # someone remembers to go looking. Claiming a feature here is a
34
+ # deliberate act (a real property, checked at least once failing
35
+ # AND once passing — `spec/fuzzing/properties_spec.rb`'s own
36
+ # discipline) or an explicit, reasoned exemption in that same
37
+ # spec — never silence.
38
+ module Properties
39
+ module_function
40
+
41
+ # WHICH LANGUAGE FEATURE EACH PROPERTY IS ANSWERABLE FOR. Not
42
+ # exhaustive of everything a property's body happens to touch —
43
+ # `Command#attributes`, say, is exercised by nearly every property
44
+ # here without being what any of them was WRITTEN to guard — but
45
+ # exhaustive of the feature that would go UNCHECKED if this
46
+ # property did not exist. That is the question the coverage gate
47
+ # actually asks.
48
+ FEATURE_COVERAGE = {
49
+ lifecycle_values_are_declared: %w[Aggregate#state_field Aggregate#state_start Aggregate#transitions
50
+ Entity#state_field Entity#state_start Entity#transitions],
51
+ saga_advances_follow_declared_handlers: %w[Handler#from_state Handler#to_state Handler#event_type],
52
+ query_answers_match_reference: %w[Query#wheres Query#order_field Query#order_way Query#limit],
53
+ paging_offset_partitions_correctly: %w[Query#options],
54
+ authorize_scopes_or_refuses: %w[Query#options],
55
+ guard_refusals_are_declared: %w[Command#givens Command#ensures],
56
+ lifecycle_guard_and_given_violations_are_refused: %w[Command#from Aggregate#preconditions Entity#preconditions],
57
+ # Dispatch#command_name/Dispatch#with_spec are NOT claimable
58
+ # feature names — META_DOMAIN_ALL_FEATURES only walks ONE level
59
+ # of entity nesting (`agg.entities.flat_map`, meta_domain_
60
+ # coverage_spec.rb), and Dispatch sits TWO deep (ProcessManager
61
+ # -> Handler -> Dispatch), so those strings never exist there
62
+ # to claim — a pre-existing meta-domain coverage-generation gap,
63
+ # found here (their old META_DOMAIN_KNOWN_GAPS entries were
64
+ # themselves already-orphaned strings no completeness check ever
65
+ # verified, since KNOWN_GAPS has no "never lets a gap rot" check
66
+ # the way FEATURE_COVERAGE/GUARANTEED_BY_CONSTRUCTION both do).
67
+ # This property still closes the REAL behavior both would have
68
+ # named — a Dispatch's own command_name/with_spec are exactly
69
+ # what dispatch_args resolves and this property checks — the
70
+ # grammar just has no feature string for either one.
71
+ dispatch_binding_fidelity: %w[Handler#dispatches Policy#with_spec],
72
+ mutations_match_recompute: %w[Command#mutations],
73
+ sagas_rehydrate_cleanly: %w[ProcessManager#states ProcessManager#correlates_by
74
+ ProcessManager#starts_on ProcessManager#ends_on],
75
+ fanout_dispatches_once_per_matching_row: %w[Policy#for_each Policy#where],
76
+ aggregation_matches_recompute: %w[ReadModel#count ReadModel#median_field],
77
+ stored_records_satisfy_declared_invariants: %w[Aggregate#invariants Entity#invariants],
78
+ group_by_matches_recompute: %w[ReadModel#group_by]
79
+ }.freeze
80
+
81
+ # FEATURES A REPLAY PROPERTY COULD NEVER CATCH VIOLATED, because the
82
+ # RUNTIME'S OWN CONSTRUCTION makes the violation impossible to
83
+ # produce in the first place — not "untested," but unfalsifiable by
84
+ # a history, the same class of guarantee this codebase already
85
+ # states for identity ("NOTHING IS MINTED" — command_interpreter.rb's
86
+ # own header) and now generalises. Each entry names the ONE place in
87
+ # the runtime that makes it true, universally, for every domain and
88
+ # every adapter — never per-domain logic a future domain could
89
+ # accidentally route around.
90
+ #
91
+ # THE BLUEBOOK/HECKSAGON BOUNDARY IS WHY THIS WORKS: a bluebook
92
+ # declares SHAPE (attribute types, patterns, closed sets, VO
93
+ # invariants — see docs/decisions/0009), and shape is enforced by
94
+ # ONE coercion door every domain's every attribute passes through
95
+ # (`Runtime::Value.build`, via value/coercion.rb + value/admission.rb)
96
+ # regardless of which hecksagon later binds the aggregate to Memory,
97
+ # Postgres, or anything else. A value that violated its own declared
98
+ # pattern, invariant, or closed set could never be COERCED, so it
99
+ # could never be STORED, so it could never appear in a replay's own
100
+ # `:instances` to be caught violating it. Checking for it after the
101
+ # fact would be watching for something the construction path already
102
+ # made impossible.
103
+ #
104
+ # NOT a place to hide a real gap — a feature belongs here only once
105
+ # the SPECIFIC enforcing code path has been read and confirmed, the
106
+ # same discipline `spec/fuzzing/meta_domain_coverage_spec.rb` demands
107
+ # of `KNOWN_GAPS` in the other direction. `Entity#identified_by` was
108
+ # checked FOR this category once before and found NOT to qualify —
109
+ # `command_interpreter.rb`'s `AlreadyExists` refusal was given to
110
+ # every CREATING AGGREGATE command uniformly, and MutationApplier
111
+ # (command_interpreter/mutation_applier.rb) had no matching check on
112
+ # an entity's own append. It does now: #check_entity_collision runs
113
+ # unconditionally on both branches an entity identity can arrive by
114
+ # (caller-supplied, or composite — the two the auto-mint branch
115
+ # doesn't cover), the same way command_interpreter#hydrate's own
116
+ # check is unconditional for every creating aggregate command. Real,
117
+ # confirmed live before the fix (SafeDepositBox's Visit/KeyIssuance —
118
+ # see spec/runtime/safe_deposit_box_spec.rb).
119
+ GUARANTEED_BY_CONSTRUCTION = {
120
+ "Aggregate#attributes" => "every field's pattern/closed-set/type passes through Value.build's one coercion door " \
121
+ "(value/coercion.rb#check_patterns, value/admission.rb) before it can exist — a stored value that violated " \
122
+ "its own declared shape was never producible to begin with",
123
+ "Aggregate#value_objects" => "the shape being coerced above — same door, same guarantee",
124
+ # S17, ADR 0026 — the list `saga_advances_follow_declared_handlers`
125
+ # (below) already walks to find each handler's own event_type/
126
+ # from_state/to_state (the three it claims) — a property cannot
127
+ # check a handler's own fields without iterating the list that
128
+ # holds them, so the list itself is exercised by the same door.
129
+ "ProcessManager#handlers" => "saga_advances_follow_declared_handlers already walks this list to find event_type/from_state/to_state — same door, same guarantee",
130
+ "Aggregate#identified_by" => "CommandInterpreter's data-driven dispatch order refuses AlreadyExists " \
131
+ "(command_interpreter.rb, command.creates?) for every creating command uniformly, before a duplicate id " \
132
+ "can ever be stored — collision is refused at the door, not produced and later caught",
133
+ "Entity#identified_by" => "MutationApplier#check_entity_collision (command_interpreter/mutation_applier.rb) " \
134
+ "checks Array(current) against every part of the entity's own identity before an append can land, on " \
135
+ "both branches identity arrives by (caller-supplied, or composite) — the same AlreadyExists refusal " \
136
+ "Aggregate#identified_by gets above, one level down. Auto-minted entities never reach the check " \
137
+ "(current.size + 1 can't repeat unless something remove:s from the list between mints, which no real " \
138
+ "domain does today — see the comment on #entity_element itself)",
139
+ "Command#attributes" => "command arguments are coerced through the SAME Value.build door as any other " \
140
+ "attribute — an accepted dispatch's own args already passed pattern/admits/invariant checks",
141
+ "Command#emits" => "CommandRules::Emission#emit iterates command.emits ITSELF to construct every announced " \
142
+ "Event (command_rules/emission.rb) — there is no other path to emit, so a command can never announce a " \
143
+ "name its own declaration doesn't list",
144
+ "Query#attributes" => "query arguments are coerced through the same Value.build door — same guarantee as " \
145
+ "Command#attributes",
146
+ "Entity#attributes" => "same coercion door, one level in — an entity's own attributes are Value-typed exactly " \
147
+ "the way an aggregate's are",
148
+ "ValueObject#attributes" => "the shape Value.build enforces IS this declaration — the guarantee and the " \
149
+ "feature are the same fact seen from two sides",
150
+ "ValueObject#invariants" => "run inside the SAME coercion call (coercion.rb, before construction returns) " \
151
+ "that pattern-checks a VO's fields — a VO whose invariant did not hold could not finish being built",
152
+ "ValueObject#rows" => "closed-set membership is checked in value/admission.rb, the second half of the same " \
153
+ "one construction door",
154
+ # S17, ADR 0026 — Member is a genuine entity now (nested under
155
+ # ValueObject), so this reads "Member#pairs", not "Member#shape" —
156
+ # the free-text, un-parsed spelling a standalone root once needed
157
+ # no longer exists at all, an entity's own element is never
158
+ # serialized as text. "ValueObject#members" is the SAME fact
159
+ # "ValueObject#rows" already counts, seen from the other side — a
160
+ # value object cannot declare admitted rows without a members list
161
+ # to hold them, and vice versa.
162
+ "ValueObject#members" => "the members list IS what ValueObject#rows counts — same door, same guarantee",
163
+ "Member#pairs" => "one level into ValueObject#rows — same door"
164
+ }.freeze
165
+
166
+ # Every lifecycle field a replay leaves an instance holding is one
167
+ # of the aggregate's OWN declared states — the full set, not just
168
+ # `Lifecycle#states`' default+targets (see ModelCheck.full_states'
169
+ # own comment on that hole). The tie to M2 is direct: the model
170
+ # checker proves which states a domain's OWN declarations can ever
171
+ # produce ; this proves a REAL RUN never produced anything else —
172
+ # a coercion bug, a stale string surviving a rename, a default
173
+ # that drifted from the declared set, would all show up here as a
174
+ # value nothing upstream would have predicted.
175
+ def lifecycle_values_are_declared(history)
176
+ bluebook = history.fetch(:bluebook)
177
+ declared = {}
178
+ bluebook.aggregates.each do |aggregate|
179
+ declared[aggregate.hecks_name] = Bluebook::ModelCheck.full_states(aggregate.lifecycle) if aggregate.lifecycle
180
+ end
181
+ return true if declared.empty?
182
+
183
+ offenders = history.fetch(:instances).filter_map do |key, state|
184
+ aggregate_name = key.split("::").last.split("#").first
185
+ states = declared[aggregate_name]
186
+ next unless states
187
+
188
+ lifecycle = bluebook.aggregate(aggregate_name).lifecycle
189
+ value = state[lifecycle.field]
190
+ next if value.nil? || states.include?(value.to_s)
191
+
192
+ "#{key} holds #{lifecycle.field}=#{value.inspect}, which #{aggregate_name} never declares as a state"
193
+ end
194
+
195
+ offenders.empty? || offenders.join("; ")
196
+ end
197
+
198
+ # Every saga advance a replay actually logged moved along an edge
199
+ # the process manager DECLARED — `(from, to)` pairs that appear in
200
+ # `saga_log` with `advanced: true` must be a `(handler.from_state,
201
+ # handler.to_state)` pair some handler on that PM declares
202
+ # (compensation edges included ; a REFUSED-triggered advance is a
203
+ # handler like any other). A saga that advanced along a pair no
204
+ # handler names would mean the runtime moved state the language
205
+ # never authorized — the same trust ModelCheck's static reachability
206
+ # rests on, checked here against what a run actually did.
207
+ def saga_advances_follow_declared_handlers(history)
208
+ bluebook = history.fetch(:bluebook)
209
+ edges = Hash.new { |h, k| h[k] = [] }
210
+ bluebook.process_managers.each do |pm|
211
+ pm.handlers.each { |handler| edges[pm.name] << [handler.from_state, handler.to_state] }
212
+ end
213
+ return true if edges.empty?
214
+
215
+ offenders = history.fetch(:sagas).filter_map do |entry|
216
+ next unless entry[:advanced]
217
+
218
+ pair = [entry[:from], entry[:to]]
219
+ next if edges[entry[:process_manager]].include?(pair)
220
+
221
+ "#{entry[:process_manager]} advanced #{pair.inspect}, which no declared handler names"
222
+ end
223
+
224
+ offenders.empty? || offenders.join("; ")
225
+ end
226
+
227
+ # THE FOUNDATIONAL ONE. `Hecks::Runtime` mints nothing — every
228
+ # identity is declared and derived, never invented (see
229
+ # command_interpreter.rb's own "NOTHING IS MINTED" — a random hex,
230
+ # a counter, anything not reproducible from the payload, was
231
+ # refused out of the runtime specifically because it broke this).
232
+ # So the SAME steps, replayed against a FRESH boot, must produce
233
+ # BYTE-IDENTICAL events, refusals, and instances — any drift here
234
+ # is nondeterminism the runtime promised not to have: a wall-clock
235
+ # read that leaked into compared state, a Hash iteration order a
236
+ # comparison depended on, anything. Two independent replays, not a
237
+ # cached one compared to itself, so a bug that corrupts the FIRST
238
+ # run's own bookkeeping cannot pass by agreeing with itself.
239
+ def replay_is_deterministic(domain_path, steps)
240
+ first = Replay.call(domain_path, steps)
241
+ second = Replay.call(domain_path, steps)
242
+
243
+ comparable = ->(history) { history.reject { |key, _| key == :bluebook || key == :bluebooks } }
244
+ return true if comparable.call(first) == comparable.call(second)
245
+
246
+ "two replays of the same #{steps.length} steps produced different histories"
247
+ end
248
+
249
+ # THE QUERY ORACLE — differential testing within the one runtime,
250
+ # the shape the retired cross-runtime harness should always have
251
+ # been. Every generated ask was answered twice at the same instant
252
+ # (Replay records both): once through whatever the aggregate is
253
+ # actually bound to (Memory's native hook is Ports::Query::InMemory;
254
+ # a SQL binding would compile it), once through the reference
255
+ # interpreter's own evaluation. The two are separate, live
256
+ # implementations of the same comparator vocabulary, and they have
257
+ # drifted before — an adapter that ACCEPTS what the reference says
258
+ # matches nothing, or orders what it refuses to order, shows up
259
+ # here as a finding no self-referential adapter spec could see.
260
+ def query_answers_match_reference(history)
261
+ offenders = history.fetch(:queries).filter_map do |asked|
262
+ next if asked[:error] || asked[:reference_rows].nil?
263
+ next if asked[:rows] == asked[:reference_rows]
264
+
265
+ "#{asked[:query]} #{asked[:args].inspect} answered #{asked[:rows].inspect} " \
266
+ "natively but #{asked[:reference_rows].inspect} through the reference interpreter"
267
+ end
268
+
269
+ offenders.empty? || offenders.join("; ")
270
+ end
271
+
272
+ # THE SAME "TWO ENGINES, COMPARED" SHAPE query_answers_match_reference
273
+ # already uses, aimed squarely at Query#options' offset/limit pair —
274
+ # but recomputed from history[:instances] directly, a THIRD,
275
+ # independent computation, rather than comparing QueryInterpreter's
276
+ # own native and reference paths against each other (which could
277
+ # share the identical bug neither implementation happened to hit —
278
+ # see #4's own fix, which touched BOTH #interpret and
279
+ # #reference_interpret at once). `order_by` declared alongside
280
+ # `offset` or `limit` names a genuinely paged query. Ports::Query::
281
+ # Ordering.apply is the SAME engine QueryInterpreter#ordered calls,
282
+ # reused here rather than re-derived, so this oracle cannot drift
283
+ # from what "in order" means without the interpreter drifting the
284
+ # identical way — only the offset-then-limit .drop/.first slice
285
+ # (#4's own fix) is independently reproduced, in plain Ruby.
286
+ #
287
+ # Real target: ATMCard.ByFee (`limit 3; offset 1`).
288
+ def paging_offset_partitions_correctly(history)
289
+ bluebooks = history.fetch(:bluebooks)
290
+
291
+ offenders = history.fetch(:queries).filter_map do |asked|
292
+ next if asked[:error] || !asked[:query].is_a?(String) || !asked[:query].include?("::")
293
+
294
+ declared = query_for_verb(bluebooks, asked[:query])
295
+ next unless declared && declared.order_by && (declared.offset || declared.limit)
296
+
297
+ domain, aggregate_name, = Naming.split_verb(asked[:query])
298
+ args = asked[:args] || {}
299
+ rows = query_eligible_rows(asked.fetch(:instances_at), domain, aggregate_name, declared.wheres, args)
300
+ ordered = Ports::Query::Ordering.apply(
301
+ rows, declared.order_by, declared.null_semantics, identity: ->(row) { row[:id].to_s }
302
+ ) { |row| Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(row, declared.order_by.field)) }
303
+
304
+ skipped = declared.offset ? ordered.drop(resolve_paging_value(declared.offset.value, args).to_i) : ordered
305
+ expected = declared.limit ? skipped.first(resolve_paging_value(declared.limit.value, args).to_i) : skipped
306
+ actual = asked[:rows]
307
+ next if actual == expected
308
+
309
+ "#{asked[:query]} #{args.inspect} answered #{actual.inspect}, but independently recomputing " \
310
+ "order/offset/limit from #{rows.length} eligible row(s) gives #{expected.inspect}"
311
+ end
312
+
313
+ offenders.empty? || offenders.join("; ")
314
+ end
315
+
316
+ # THE DECLARED Query ITSELF, resolved from a replayed verb — the
317
+ # same shape #command_for_verb resolves a command by, one
318
+ # construct over. Entity-level queries (a dotted query_path) are
319
+ # out of scope here — paging on an entity's own list has no real
320
+ # corpus site yet, and the "one many-side head, one aggregate,
321
+ # no FK-join" shape #query_eligible_rows assumes doesn't hold for
322
+ # one.
323
+ def query_for_verb(bluebooks, verb)
324
+ domain, aggregate_name, query_path = Naming.split_verb(verb)
325
+ return nil unless query_path && !query_path.include?(".")
326
+
327
+ bluebook = bluebooks[domain]
328
+ aggregate = bluebook&.aggregate(aggregate_name)
329
+ aggregate&.query(query_path)
330
+ end
331
+
332
+ # A QUERY'S OWN ROWS — unlike #eligible_rows (a ReadModel's
333
+ # reduced/grouped many-side head, possibly FK-joined against a
334
+ # root), a Query always asks about its OWN owning aggregate
335
+ # directly ; no join, no reference_target. `id:` merged in the
336
+ # same way #eligible_rows' own rows are, since a stable sort
337
+ # (Ordering.apply's own `identity:`) and the real answer's own
338
+ # `record.state.merge(id: record.id)` both need it.
339
+ def query_eligible_rows(instances, domain, aggregate_name, wheres, args)
340
+ prefix = "#{domain}::#{aggregate_name}#"
341
+ instances.filter_map do |key, state|
342
+ next unless key.start_with?(prefix)
343
+
344
+ row = state.merge(id: key.split("#").last)
345
+ next unless wheres.all? do |clause|
346
+ held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(row, clause.field))
347
+ Ports::Query::InMemory.holds?(clause, held, args)
348
+ end
349
+
350
+ row
351
+ end
352
+ end
353
+
354
+ # `QueryInterpreter#resolve_query_value`, reproduced: a declared
355
+ # limit/offset is either a literal or a Symbol naming an argument
356
+ # the caller supplied.
357
+ def resolve_paging_value(value, args)
358
+ value.is_a?(Symbol) ? args[value] : value
359
+ end
360
+
361
+ # `Query#options`' OTHER HALF — TenantScope.apply's own contract
362
+ # (tenant_scope.rb), independently restated as a property rather
363
+ # than exercised only through whatever the generator happens to
364
+ # try. NOT closed by the generator here on purpose: SafeDepositBox.
365
+ # Rented — the only real corpus query declaring `authorize` at
366
+ # all — declares ZERO attributes of its own, so StepBuilder#args_for
367
+ # always hands it `{}` and TenantScope.apply refuses every
368
+ # generated attempt, unconditionally (confirmed: no successful ask
369
+ # against an authorize-bearing query reaches this property via the
370
+ # standard battery today). Extending the generator to invent a
371
+ # `tenant:` value ran into a separate, real finding along the way —
372
+ # SafeDepositBox is COMPOSITE-identified (`identified_by` is nil
373
+ # for it — Runtime::Identified#derive_identity), so the generator's
374
+ # existing `known_ids` pool (keyed by `aggregate.identified_by ||
375
+ # "id"`) tracks a stray, never-real scalar for it rather than its
376
+ # true `branch_code`+`box_number` pair — a second, narrower
377
+ # generator gap this property does not attempt to fix, since fixing
378
+ # it well enough to trust a generated `tenant:` value would be the
379
+ # heavier, "benefits every future property" path the plan itself
380
+ # names as the alternative. Hand-built fixtures close the real
381
+ # claim directly instead: faster, narrower, and correct either way,
382
+ # since TenantScope.apply's contract is identical regardless of
383
+ # where a `tenant:` arg came from.
384
+ #
385
+ # Two claims, matching TenantScope.apply's own two branches: every
386
+ # SUCCESSFUL answer's own tenant field agrees with the tenant arg
387
+ # given (the WhereClause TenantScope injects is a Symbol reference
388
+ # into args, resolved dynamically — this checks the OUTCOME, not
389
+ # re-deriving that resolution) ; every ask MISSING a required
390
+ # tenant: refuses with the declared wording, never succeeds. A
391
+ # refusal for an unrelated reason with the tenant arg present is
392
+ # not this property's claim either way — skipped, not graded.
393
+ def authorize_scopes_or_refuses(history)
394
+ bluebooks = history.fetch(:bluebooks)
395
+
396
+ offenders = history.fetch(:queries).filter_map do |asked|
397
+ next unless asked[:query].is_a?(String) && asked[:query].include?("::")
398
+
399
+ declared = query_for_verb(bluebooks, asked[:query])
400
+ tenant = declared&.authorization&.tenant&.to_sym
401
+ next unless tenant
402
+
403
+ args = asked[:args] || {}
404
+ tenant_given = args.key?(tenant)
405
+
406
+ if asked[:error]
407
+ next if tenant_given
408
+ next if asked[:error].to_s.include?("declares authorize with tenant: #{tenant}")
409
+
410
+ "#{asked[:query]} #{args.inspect} refused with no #{tenant}: given, but not with the declared " \
411
+ "tenant_required wording (#{asked[:error]})"
412
+ elsif !tenant_given
413
+ "#{asked[:query]} #{args.inspect} answered successfully with no #{tenant}: given, but #{declared.name} " \
414
+ "declares authorize with tenant: #{tenant}"
415
+ else
416
+ wanted = args[tenant].to_s
417
+ mismatched = asked[:rows].find do |row|
418
+ Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(row, tenant)).to_s != wanted
419
+ end
420
+ next unless mismatched
421
+
422
+ "#{asked[:query]} #{args.inspect} answered a row whose #{tenant} disagrees with the given " \
423
+ "#{wanted.inspect}: #{mismatched.inspect}"
424
+ end
425
+ end
426
+
427
+ offenders.empty? || offenders.join("; ")
428
+ end
429
+
430
+ # EVERY GIVEN/ENSURES REFUSAL A RUN ACTUALLY RAISED NAMES A RULE
431
+ # THE COMMAND ACTUALLY DECLARES. `GivenNotMet`/`EnsuresNotMet` both
432
+ # quote their guard's own `description` verbatim
433
+ # (command_rules/admissibility.rb: `"#{command.hecks_name} refused
434
+ # — #{given.description}"`) — the SAME text `behavior.bluebook`'s
435
+ # own `Rule`/Command.Ensure hold as `Rule#description`, so a
436
+ # refusal whose quoted text is not among the refusing command's
437
+ # OWN `guard_descriptions` (Behaviour::Command, both givens and
438
+ # ensures) is either a stale message surviving a renamed rule, a
439
+ # rule firing against the wrong command's own guard set, or the
440
+ # wording drifting out from under the declaration it is supposed
441
+ # to quote — banking's own 128 status givens (customer/account
442
+ # guards, some through a cross-aggregate dereference) are exactly
443
+ # the surface this exists to hold to its word.
444
+ #
445
+ # `kind:` is what tells a guard refusal apart from the FOUR other
446
+ # `RefusalWording` templates sharing the identical "X refused — Y"
447
+ # shape (LifecycleRefused/transition_blocked, both TypeMismatch
448
+ # object-reference templates, Unauthorized/role_mismatch) — see
449
+ # Replay's own comment at the refusal rescue site. Pattern-matching
450
+ # the string alone would confuse a guard's own wording with any of
451
+ # those; the raised class does not.
452
+ GUARD_REFUSAL_KINDS = %w[Hecks::Runtime::GivenNotMet Hecks::Runtime::EnsuresNotMet].freeze
453
+
454
+ def guard_refusals_are_declared(history)
455
+ bluebooks = history.fetch(:bluebooks)
456
+
457
+ offenders = history.fetch(:refusals).filter_map do |refusal|
458
+ next unless GUARD_REFUSAL_KINDS.include?(refusal[:kind])
459
+
460
+ match = refusal[:error].to_s.match(/\A(.+) refused — (.+)\z/)
461
+ next "#{refusal[:verb]} raised #{refusal[:kind]} with unparseable message #{refusal[:error].inspect}" unless match
462
+
463
+ command = command_for_verb(bluebooks, refusal[:verb])
464
+ next "#{refusal[:verb]} raised #{refusal[:kind]}, but no declared command resolves that verb" unless command
465
+ next if command.guard_descriptions.include?(match[2])
466
+
467
+ "#{refusal[:verb]} refused — #{match[2].inspect} — but #{command.hecks_name} declares no given " \
468
+ "or ensures with that description (it declares #{command.guard_descriptions.inspect})"
469
+ end
470
+
471
+ offenders.empty? || offenders.join("; ")
472
+ end
473
+
474
+ # A DECLARED PROCESS MANAGER'S OWN COMMAND — `command.hecks_name`,
475
+ # or an entity's own if the verb's second component is itself
476
+ # dotted (`Aggregate.Entity.Command`, the same two shapes
477
+ # `Dispatcher#dispatch` itself branches on). Shared by the guard
478
+ # property above and available for anything else that needs to go
479
+ # from a replayed verb back to its declaration.
480
+ #
481
+ # RESOLVED AGAINST `bluebooks` (the FULL map, `history[:bluebooks]`
482
+ # — every loaded domain, keyed by name), never a single assumed
483
+ # bluebook: a verb names its OWN domain (`Naming.split_verb`'s
484
+ # first element), and that domain is not always the one Replay
485
+ # happens to expose as `history[:bluebook]`. A fuzz run against
486
+ # `lib/hecks/grammar` (Expression + Translation, in load
487
+ # order) found this the hard way — every `Translation::Map.Seal`
488
+ # refusal read as "no declared command resolves that verb" purely
489
+ # because `history[:bluebook]` was Expression, not Translation; the
490
+ # refusal was real, this property's own domain resolution was not.
491
+ def command_for_verb(bluebooks, verb)
492
+ domain, aggregate_name, command_path = Naming.split_verb(verb)
493
+ return nil unless command_path
494
+
495
+ bluebook = bluebooks[domain]
496
+ return nil unless bluebook
497
+
498
+ aggregate = bluebook.aggregate(aggregate_name)
499
+ return nil unless aggregate
500
+
501
+ if command_path.include?(".")
502
+ entity_name, sub = command_path.split(".", 2)
503
+ entity = aggregate.entities.find { |e| e.hecks_name == entity_name }
504
+ entity&.command(sub)
505
+ else
506
+ aggregate.command(command_path)
507
+ end
508
+ end
509
+
510
+ # `guard_refusals_are_declared`'s OWN OPPOSITE DIRECTION. That
511
+ # property is passive and one-directional — for a refusal that
512
+ # ALREADY HAPPENED, is the quoted text real declared text? It says
513
+ # nothing about a guard that should have refused and silently did
514
+ # not — a call site that stopped calling enforce_givens/enforce_
515
+ # lifecycle_guard would never appear in history[:refusals] at all,
516
+ # invisible to that property by construction.
517
+ #
518
+ # This one calls Admissibility#enforce_givens (which itself folds
519
+ # in #enforce_lifecycle_guard whenever `declaring:` is passed)
520
+ # DIRECTLY, against Replay's own pre-dispatch snapshot
521
+ # (history[:guard_checks], one bounded, additive extension — see
522
+ # that file's own comment at the capture site) — an independent
523
+ # recomputation, not grading production against itself, the same
524
+ # "two engines, compared" shape query_answers_match_reference and
525
+ # the fan-out oracle already establish. `recomputed_refused`
526
+ # (Replay's own call, made live, before this step's real dispatch
527
+ # could mutate anything a cross-aggregate given dereferences) is
528
+ # compared against `actual_refused` (GivenNotMet/LifecycleRefused
529
+ # specifically — Replay's own comment on GUARD_REFUSAL_CLASSES
530
+ # explains why ANY other refusal class, or an outright success,
531
+ # both count as "the guard did not fire," since enforce_givens
532
+ # runs FIRST in DISPATCH_ORDER).
533
+ #
534
+ # Aggregate#preconditions closes for free alongside this — a
535
+ # no-block `given` reference (CommandBuilder#given) pushes the
536
+ # SAME Given struct object `enforce_givens` already iterates
537
+ # command.givens for, so there is no separate runtime path a
538
+ # property could exercise beyond what this already reaches.
539
+ # Entity#preconditions closes the identical way, one level down
540
+ # (ADR 0028) — a piece's own bare `given` reference pushes the
541
+ # SAME Given struct onto ITS OWN referencing command's givens,
542
+ # so LedgerEntry's own Amend/Reverse (banking) already exercise
543
+ # this through the exact mechanism above, no separate path.
544
+ #
545
+ # Real targets: Account.Debit/CloseAccount (`from:` guards),
546
+ # Credit/Debit (the named-once `given("customer is active")`
547
+ # precondition) — FreezeAccount deliberately references the
548
+ # DIFFERENT named precondition `"customer is not closed"` instead
549
+ # (a suspended customer must still be freezable), so it is not a
550
+ # `"customer is active"` example, just the same MECHANISM.
551
+ def lifecycle_guard_and_given_violations_are_refused(history)
552
+ offenders = history.fetch(:guard_checks).filter_map do |check|
553
+ next if check[:recomputed_refused] == check[:actual_refused]
554
+
555
+ "#{check[:verb]} — independently recomputing enforce_givens/enforce_lifecycle_guard against the " \
556
+ "pre-dispatch state says #{check[:recomputed_refused] ? "refused (#{check[:recomputed_kind]})" : 'admitted'}, " \
557
+ "but the real dispatch #{check[:actual_refused] ? "refused (#{check[:actual_kind]})" : 'admitted it'}"
558
+ end
559
+
560
+ offenders.empty? || offenders.join("; ")
561
+ end
562
+
563
+ # Closes Handler#dispatches and — the same shape, one construct
564
+ # over — Policy#with_spec (Dispatch#command_name/Dispatch#with_spec
565
+ # are not claimable feature strings at all — see FEATURE_COVERAGE's
566
+ # own comment on this entry). history[:saga_dispatches]/[:policy_dispatches]
567
+ # (Registry#saga_dispatch_log/#policy_dispatch_log — additive,
568
+ # Ruby-only, NEVER touching saga_log/reaction_log, the byte-for-
569
+ # byte shape spec/rust_conformance_spec.rb holds Rust to) each
570
+ # carry the RAW inputs a dispatch's own args were resolved from,
571
+ # captured live at the moment the resolution actually ran — a
572
+ # saga's own memory keeps changing across a run, so re-deriving
573
+ # from history[:saga_instances]'s FINAL memory (the only other
574
+ # place it would be visible) would grade the wrong moment,
575
+ # lifecycle_guard_and_given_violations_are_refused's own false
576
+ # positive one item earlier, in a different shape.
577
+ #
578
+ # #resolve_dispatch_binding/#resolve_trigger_binding are SEPARATE,
579
+ # independently-written re-derivations of SagaInterpreter#
580
+ # dispatch_args/PolicyInterpreter#trigger_args's own resolution —
581
+ # never calling either method again, which would only ever agree
582
+ # with itself. Exactly the class of bug this closes: "a wrong
583
+ # argument binding on a fan-out dispatch that produces a perfectly
584
+ # normal-looking log entry (`delivered: true`) and would only ever
585
+ # surface as a downstream assertion failure, if it surfaces at
586
+ # all" — PR #325's own defect class, one level over.
587
+ #
588
+ # Real targets: Settlement (mixed literal/correlation-head/event-
589
+ # payload/memory-fallback bindings across three legs, plus a
590
+ # compensation leg that deliberately omits `reference:`, a field
591
+ # the forward Credit leg carries), ExternalSettlement, Onboarding
592
+ # (no compensation leg, by design — nothing to check there beyond
593
+ # the forward leg's own event-payload binding).
594
+ def dispatch_binding_fidelity(history)
595
+ saga_offenders = history.fetch(:saga_dispatches, []).filter_map do |entry|
596
+ expected = resolve_dispatch_binding(entry)
597
+ next if expected == entry[:args]
598
+
599
+ "#{entry[:process_manager]}##{entry[:instance]} dispatching #{entry[:dispatch]} on #{entry[:on]} — " \
600
+ "bound #{entry[:args].inspect}, but independently re-deriving with_spec's own resolution gives " \
601
+ "#{expected.inspect}"
602
+ end
603
+
604
+ policy_offenders = history.fetch(:policy_dispatches, []).filter_map do |entry|
605
+ expected = resolve_trigger_binding(entry)
606
+ next if expected == entry[:args]
607
+
608
+ "#{entry[:policy]} on #{entry[:on]} — bound #{entry[:args].inspect}, but independently re-deriving " \
609
+ "with_spec's own resolution gives #{expected.inspect}"
610
+ end
611
+
612
+ offenders = saga_offenders + policy_offenders
613
+ offenders.empty? || offenders.join("; ")
614
+ end
615
+
616
+ # SagaInterpreter#dispatch_args's own 4-branch resolution,
617
+ # reproduced independently: a literal, the correlation key itself,
618
+ # the CURRENT triggering event's own payload, or — the fallback —
619
+ # the saga's own carried memory (seeded from the STARTING event's
620
+ # payload, at begin_saga).
621
+ def resolve_dispatch_binding(entry)
622
+ entry[:with_spec].to_h do |key, value|
623
+ resolved = if !value.is_a?(Symbol) then value
624
+ elsif value == entry[:correlation_head] then entry[:instance]
625
+ elsif entry[:event_payload].key?(value) then entry[:event_payload][value]
626
+ else entry[:memory][value]
627
+ end
628
+ [key.to_sym, Runtime::Value.materialize(resolved)]
629
+ end
630
+ end
631
+
632
+ # PolicyInterpreter#trigger_args's own 2-branch resolution — a
633
+ # policy holds no correlation and no memory, so `payload` (the
634
+ # triggering event's own payload, already merged with a fan-out
635
+ # row's id when there is one) is the WHOLE source.
636
+ def resolve_trigger_binding(entry)
637
+ entry[:with_spec].to_h do |key, value|
638
+ resolved = value.is_a?(Symbol) ? entry[:payload][value] : value
639
+ [key.to_sym, Runtime::Value.materialize(resolved)]
640
+ end
641
+ end
642
+
643
+ # Closes Command#mutations — the last of the five, and the
644
+ # largest: no real corpus entity anywhere uses `append`/`remove`/
645
+ # `multiply`/`clamp` (every real aggregate-owned mutation is
646
+ # aggregate-scoped — Account.Credit's :ledger, LogVisit's
647
+ # :visits); the only entity-owned use of these four ops in the
648
+ # whole repository is spec/fixtures/entity_list_mutations, now a
649
+ # real, bootable, Memory-default domain (no .hecksagon needed at
650
+ # all — a domain with none boots every aggregate against Memory
651
+ # by construction, confirmed live) rather than the raw-Kernel.
652
+ # load-only fixture it was.
653
+ #
654
+ # `history[:mutation_traces]` (Replay's own bounded, additive
655
+ # extension — see #build_mutation_trace's own comment) carries a
656
+ # per-step before/after snapshot of the ENTITY ELEMENT an
657
+ # entity-dispatched command's own mutations acted on, materialized
658
+ # to plain data, plus the step's own raw args — the delta
659
+ # `aggregation_matches_recompute` never had to ask for, because
660
+ # count/median are pure functions of FINAL state and a mutation
661
+ # is not (the same "captured live, not re-derived from final
662
+ # state" lesson item 8's own saga_dispatch_log already learned).
663
+ #
664
+ # #recompute_append/#recompute_remove/#recompute_multiply/
665
+ # #recompute_clamp are SEPARATE, independently-written
666
+ # reproductions of MutationApplier#appended/#removed and
667
+ # CommandRules::Arithmetic#multiply/#clamp — never calling either
668
+ # again, which would only ever agree with itself. `:set`/
669
+ # `:increment`/`:decrement` are out of scope on purpose (the four
670
+ # "vendored, not yet upstream" ops this item exists for); a
671
+ # command mixing them with a recomputable op still gets the
672
+ # recomputable one checked.
673
+ #
674
+ # `:unrecomputable` (never compared, never a finding) covers the
675
+ # generator's own deliberate arg-malforming (`StepBuilder#malform`)
676
+ # landing a non-Numeric amount/non-2-element bounds where
677
+ # multiply/clamp need one — the SAME shape `guard_check`'s own
678
+ # AbsentArgument false positive taught: a step whose raw material
679
+ # doesn't fit the op's own contract is inconclusive, not a claimed
680
+ # mismatch.
681
+ RECOMPUTABLE_MUTATION_OPS = %i[append remove multiply clamp].freeze
682
+
683
+ def mutations_match_recompute(history)
684
+ bluebooks = history.fetch(:bluebooks)
685
+
686
+ offenders = history.fetch(:mutation_traces, []).flat_map do |entry|
687
+ next [] unless entry[:after]
688
+
689
+ command = command_for_verb(bluebooks, entry[:verb])
690
+ next [] unless command
691
+
692
+ command.mutations.select { |m| RECOMPUTABLE_MUTATION_OPS.include?(m.op) }.filter_map do |mutation|
693
+ expected = recompute_mutation(mutation, entry[:before][mutation.target], entry[:args], entry[:before])
694
+ next if expected == :unrecomputable
695
+
696
+ actual = entry[:after][mutation.target]
697
+ next if symbolize_deep(expected) == symbolize_deep(actual)
698
+
699
+ "#{entry[:verb]} — #{mutation.op} on #{mutation.target} — recomputing independently gives " \
700
+ "#{expected.inspect}, but the real dispatch left #{actual.inspect}"
701
+ end
702
+ end
703
+
704
+ offenders.empty? || offenders.join("; ")
705
+ end
706
+
707
+ def recompute_mutation(mutation, current, args, before_scope)
708
+ case mutation.op
709
+ when :append then recompute_append(current, mutation.source, before_scope, args)
710
+ when :remove then recompute_remove(current, mutation.source, args)
711
+ when :multiply then recompute_multiply(current, resolve_mutation_source(mutation.source, args))
712
+ when :clamp then recompute_clamp(current, mutation.source)
713
+ end
714
+ end
715
+
716
+ # `MutationApplier#appended`'s own value-object branch (never the
717
+ # entity_element branch — see #build_mutation_trace's own comment
718
+ # on why an entity-dispatched command's own mutations never reach
719
+ # it), reproduced: the field map resolved the SAME two-tier way
720
+ # (`MutationApplier#resolve_append_source` — a caller-supplied
721
+ # arg, or the entity's own current field), then appended.
722
+ def recompute_append(current, source_map, before_scope, args)
723
+ fields = source_map.transform_values { |source| resolve_mutation_append_field(source, before_scope, args) }
724
+ Array(current) + [symbolize_deep(fields)]
725
+ end
726
+
727
+ def resolve_mutation_append_field(source, before_scope, args)
728
+ return source unless source.is_a?(Symbol)
729
+ return args[source] if args.key?(source)
730
+
731
+ before_scope[source]
732
+ end
733
+
734
+ # `MutationApplier#removed`'s own value-equality match, reproduced.
735
+ def recompute_remove(current, source, args)
736
+ target = symbolize_deep(resolve_mutation_source(source, args))
737
+ Array(current).reject { |element| symbolize_deep(element) == target }
738
+ end
739
+
740
+ # `CommandRules::Arithmetic#multiply`'s own two branches,
741
+ # reproduced on plain materialized data instead of a real Value:
742
+ # a single-numeric-field Hash (the VO-typed case — ListCount, one
743
+ # Integer field) scales that field ; a bare Numeric scales itself.
744
+ # `current ||= 0` — the SAME phantom-field fallback #multiply
745
+ # itself already gives (unaffected by this session's #clamp fix,
746
+ # since #multiply never needed one).
747
+ def recompute_multiply(current, amount)
748
+ return :unrecomputable unless amount.is_a?(Numeric)
749
+
750
+ current ||= 0
751
+ if current.is_a?(Hash)
752
+ field = current.keys.find { |f| current[f].is_a?(Numeric) }
753
+ return :unrecomputable unless field
754
+
755
+ current.merge(field => current[field] * amount)
756
+ elsif current.is_a?(Numeric)
757
+ current * amount
758
+ else
759
+ :unrecomputable
760
+ end
761
+ end
762
+
763
+ # `CommandRules::Arithmetic#clamp`'s own two branches, reproduced
764
+ # the same way #recompute_multiply is — including THIS SESSION'S
765
+ # OWN `current ||= 0` fix (command_rules/arithmetic.rb), the one
766
+ # arithmetic op that didn't have it until now. `mutation.source`
767
+ # is always a literal `[min, max]`, never an argument reference
768
+ # (MutationApplier's own comment on why `resolve_source` is
769
+ # skipped for clamp) — so nothing here reads `args` for it at all.
770
+ def recompute_clamp(current, bounds)
771
+ return :unrecomputable unless bounds.is_a?(Array) && bounds.size == 2
772
+
773
+ min, max = bounds
774
+ current ||= 0
775
+ if current.is_a?(Hash)
776
+ field = current.keys.find { |f| current[f].is_a?(Numeric) }
777
+ return :unrecomputable unless field
778
+
779
+ current.merge(field => current[field].clamp(min, max))
780
+ elsif current.is_a?(Numeric)
781
+ current.clamp(min, max)
782
+ else
783
+ :unrecomputable
784
+ end
785
+ end
786
+
787
+ # `CommandRules::Arithmetic#resolve_source`, reproduced: a
788
+ # mutation's source is either the NAME OF AN ARGUMENT or a
789
+ # LITERAL, told apart by type.
790
+ def resolve_mutation_source(source, args)
791
+ source.is_a?(Symbol) ? args[source] : source
792
+ end
793
+
794
+ # A generated step's own `args` arrive with STRING keys on every
795
+ # nested Hash (the wire/JSON shape `spec/corpus/*.json` already
796
+ # uses) while `history[:mutation_traces]`' own materialized
797
+ # before/after state carries SYMBOL keys throughout (Runtime::
798
+ # Value.materialize's own convention) — two hashes holding the
799
+ # identical fact compare UNEQUAL by Ruby's own `Hash#==` unless
800
+ # both sides are normalized the same way first. Recursive, since
801
+ # an appended/removed element can itself nest a value object
802
+ # (RemoveTag's own `Tag` argument, `{"key"=>..., "value"=>...}`).
803
+ def symbolize_deep(value)
804
+ case value
805
+ when Hash then value.to_h { |key, val| [key.to_sym, symbolize_deep(val)] }
806
+ when Array then value.map { |val| symbolize_deep(val) }
807
+ else value
808
+ end
809
+ end
810
+
811
+ # EVERY STORED RECORD STILL SATISFIES ITS OWN AGGREGATE'S DECLARED
812
+ # INVARIANTS — Admissibility#enforce_invariants (command_rules/
813
+ # admissibility.rb) checks these AFTER every command's mutations,
814
+ # BEFORE save, the same point `ensures` is checked. Nothing until
815
+ # now re-checked a record AFTER a whole replay finished, independent
816
+ # of whichever call site was supposed to have refused a violation
817
+ # in the first place — a record failing its own declared invariant
818
+ # here is proof a violating write landed anyway: the call site
819
+ # stopped calling enforce_invariants, or some other path (a
820
+ # translation, a backfill) wrote around it entirely.
821
+ #
822
+ # `history[:instances]` entries are already plain, symbol-keyed
823
+ # state Hashes (Replay.call's own `record.state`) — called against
824
+ # Evaluator.call the SAME way ValueObject::Builder#build already
825
+ # does for a VO's own invariants (value/coercion.rb), no GuardState
826
+ # wrapper needed the way enforce_invariants' own LIVE call uses one
827
+ # (GuardState exists for `parent.`/projected-field dereferencing
828
+ # mid-dispatch; a stored record's own scalar fields need none of
829
+ # that to re-check a same-aggregate invariant against itself).
830
+ #
831
+ # Real target: Account's own `invariant("the balance never goes
832
+ # negative") { balance.cents >= 0 }`.
833
+ #
834
+ # Entity#invariants (round 7) closes here too, not for free —
835
+ # `stored_records_satisfy_declared_invariants` only ever checked
836
+ # the AGGREGATE's own flat state; a piece's own invariant is
837
+ # checked against every ELEMENT of a `list_of` field, a genuinely
838
+ # different walk `check_piece_invariants` below makes,
839
+ # independently of `Admissibility#check_entity_invariants` (the
840
+ # live enforcement path this property exists to catch drifting
841
+ # from) — same reasoning `stored_records_satisfy_declared_
842
+ # invariants`' own top-level check already applies one level up.
843
+ #
844
+ # Real target: SafeDepositBox's own Visit — `invariant("a written
845
+ # note is not blank") { !note || !note.text.to_s.empty? }`.
846
+ def stored_records_satisfy_declared_invariants(history)
847
+ bluebooks = history.fetch(:bluebooks)
848
+
849
+ offenders = history.fetch(:instances).filter_map do |key, state|
850
+ domain_name = key.split("::").first
851
+ aggregate_name = key.split("::").last.split("#").first
852
+ bluebook = bluebooks[domain_name]
853
+ aggregate = bluebook&.aggregate(aggregate_name)
854
+ next unless aggregate
855
+
856
+ violated = aggregate.invariants.find do |invariant|
857
+ !Bluebook::Expression::Evaluator.call(invariant.canonical, state)
858
+ end
859
+ next "#{key} violates #{aggregate_name}'s own declared invariant #{violated.description.inspect}" if violated
860
+
861
+ check_piece_invariants(aggregate, state, key)
862
+ end
863
+
864
+ offenders.empty? || offenders.join("; ")
865
+ end
866
+
867
+ # A PIECE'S OWN INVARIANT, checked against every element a
868
+ # `list_of` field holds — the SAME lookup `Admissibility#
869
+ # check_entity_invariants` makes (`owner.attributes.find { |a|
870
+ # a.list? && a.type.to_s == entity.hecks_name }`), independently
871
+ # reapplied here against a STORED record's own plain Hash state
872
+ # rather than a live `Instance`.
873
+ def check_piece_invariants(owner_construct, owner_state, key)
874
+ owner_construct.entities.each do |entity|
875
+ next if entity.invariants.empty?
876
+
877
+ list_attr = owner_construct.attributes.find { |a| a.list? && a.type.to_s == entity.hecks_name }
878
+ next unless list_attr
879
+
880
+ Array(owner_state[list_attr.name]).each do |element|
881
+ violated = entity.invariants.find do |invariant|
882
+ !Bluebook::Expression::Evaluator.call(invariant.canonical, element)
883
+ end
884
+ return "#{key}'s own #{entity.hecks_name} violates its declared invariant #{violated.description.inspect}" if violated
885
+
886
+ nested = check_piece_invariants(entity, element, key)
887
+ return nested if nested
888
+ end
889
+ end
890
+ nil
891
+ end
892
+
893
+ # A SAGA INSTANCE'S OWN CHECKPOINT SURVIVES BEING WRITTEN AND READ
894
+ # BACK — the durability contract `SagaInterpreter#checkpoint` makes
895
+ # (`state:` plus a `deep_copy`d `memory:`, handed to whatever
896
+ # adapter answers `save_saga`) and `Registry#rehydrate_sagas!`
897
+ # promises to restore on the next boot (`each_saga` yielding
898
+ # `[pm, correlation, state, memory]` back into `saga_instances`).
899
+ # `Replay` captures the LIVE store already materialised the same
900
+ # way `checkpoint` itself does (`Value.materialize`, not raw
901
+ # `Runtime::Value`s — see its own comment); this property pushes
902
+ # that captured memory through the SAME `JSON.generate` then
903
+ # `JSON.parse(symbolize_names: true)` round-trip `checkpoint`'s own
904
+ # `deep_copy` performs (mirrored here rather than called — a
905
+ # private instance method with no registry to hand it) and checks
906
+ # it comes back byte-identical. A memory holding anything that
907
+ # round-trip cannot carry faithfully — a bare Symbol leaf, a
908
+ # non-JSON type a future field introduces — is corruption the
909
+ # durable path would introduce on a REAL restart, caught here
910
+ # without needing one.
911
+ #
912
+ # `declares_state?` (Behaviour::ProcessManager) is the other half:
913
+ # a live or rehydrated instance sitting in a state the procedure
914
+ # never declares is the saga-durability twin of
915
+ # `lifecycle_values_are_declared` above.
916
+ def sagas_rehydrate_cleanly(history)
917
+ bluebook = history.fetch(:bluebook)
918
+ process_managers = bluebook.process_managers.to_h { |pm| [pm.name, pm] }
919
+
920
+ offenders = history.fetch(:saga_instances).flat_map do |pm_name, conversations|
921
+ pm = process_managers[pm_name]
922
+
923
+ conversations.filter_map do |correlation, instance|
924
+ problems = []
925
+
926
+ problems << "holds state #{instance[:state].inspect}, which #{pm_name} never declares" if pm && !pm.declares_state?(instance[:state])
927
+
928
+ rehydrated = JSON.parse(JSON.generate(instance[:memory]), symbolize_names: true)
929
+ if rehydrated != instance[:memory]
930
+ problems << "memory does not survive its own checkpoint round-trip " \
931
+ "(checkpointed #{instance[:memory].inspect}, rehydrated #{rehydrated.inspect})"
932
+ end
933
+
934
+ next if problems.empty?
935
+
936
+ "#{pm_name}##{correlation.inspect}: #{problems.join(' and ')}"
937
+ end
938
+ end
939
+
940
+ offenders.empty? || offenders.join("; ")
941
+ end
942
+
943
+ # A `for_each` POLICY DISPATCHES EXACTLY ONCE PER ROW ITS DECLARED
944
+ # QUERY ANSWERS — never once for the triggering event regardless of
945
+ # row count, never skipping a matched row, never firing on a row a
946
+ # concurrent mutation only made match AFTER the fact. `Replay`
947
+ # computes the expected row-id set INDEPENDENTLY, at the same
948
+ # instant the real dispatch runs (`Replay.expected_fan_out_rows`,
949
+ # the query oracle's own shape aimed at fan-out: two engines
950
+ # compared, never one graded against itself), and records it
951
+ # beside what the reaction log actually shows. `expected_row_ids`
952
+ # is `nil`, not `[]`, when `policy.where` did not hold — no
953
+ # dispatch is the claim then, not "dispatched to zero rows," and a
954
+ # policy that dispatched anyway despite a failing guard is as real
955
+ # a finding as a row it skipped.
956
+ def fanout_dispatches_once_per_matching_row(history)
957
+ offenders = history.fetch(:fan_outs).filter_map do |finding|
958
+ expected = finding[:expected_row_ids]
959
+ actual = finding[:actual_row_ids].sort
960
+
961
+ if expected.nil?
962
+ next if actual.empty?
963
+
964
+ next "#{finding[:policy]} on #{finding[:on]}: where did not hold, but dispatched to #{actual.inspect}"
965
+ end
966
+
967
+ next if actual == expected
968
+
969
+ "#{finding[:policy]} on #{finding[:on]}: for_each answered #{expected.inspect}, " \
970
+ "but the reaction log shows dispatches to #{actual.inspect}"
971
+ end
972
+
973
+ offenders.empty? || offenders.join("; ")
974
+ end
975
+
976
+ # A `count`/`median` REPORT'S REDUCED SCALAR MATCHES THE SAME
977
+ # REDUCTION DONE INDEPENDENTLY, over the SAME eligible rows —
978
+ # `ReadModelInterpreter#project`'s own FK-join (root first, then
979
+ # each many-side head matched against it) and `#median` (odd →
980
+ # the true middle, even → the average of the two middles as a
981
+ # Float, empty → `nil`; `count` is the filtered length, empty →
982
+ # `0`), reproduced here in plain Ruby against `history[:instances]`
983
+ # rather than a live registry — `FieldPath.dig` +
984
+ # `Ports::Query::InMemory.comparable`/`.holds?` are the SAME two
985
+ # calls the interpreter itself makes to read a field and judge a
986
+ # `where`, called here rather than re-derived, so this oracle
987
+ # cannot drift from what "read a field" or "a clause holds" mean
988
+ # without the interpreter drifting the identical way.
989
+ #
990
+ # Only a report whose `:query` is answered by the SAME bluebook
991
+ # `history[:bluebook]` carries (the bare `Domain.report_name`
992
+ # form, `domain == bluebook.name`) is checked — the same "only
993
+ # what we have the grammar for" scope `lifecycle_values_are_declared`
994
+ # already takes for a multi-domain replay.
995
+ def aggregation_matches_recompute(history)
996
+ bluebook = history.fetch(:bluebook)
997
+
998
+ offenders = history.fetch(:queries).filter_map do |asked|
999
+ next if asked[:error]
1000
+
1001
+ domain, name = asked[:query].to_s.split(".", 2)
1002
+ next unless name && domain == bluebook.name
1003
+
1004
+ model = bluebook.read_model(name)
1005
+ next unless model && (model.count? || model.median_field)
1006
+
1007
+ reduced_head = model.aggregate_heads.find { |head| head[:many] }
1008
+ next unless reduced_head
1009
+
1010
+ rows = eligible_rows(bluebook, asked.fetch(:instances_at), domain, model, reduced_head, asked[:args] || {})
1011
+ expected = model.count? ? rows.length : recompute_median(rows, model.median_field)
1012
+ actual = asked[:rows]&.first&.dig(reduced_head[:as])
1013
+ next if actual == expected
1014
+
1015
+ "#{asked[:query]} #{asked[:args].inspect} answered #{actual.inspect} for #{reduced_head[:as]}, " \
1016
+ "but recomputing independently from #{rows.length} eligible row(s) gives #{expected.inspect}"
1017
+ end
1018
+
1019
+ offenders.empty? || offenders.join("; ")
1020
+ end
1021
+
1022
+ # `aggregation_matches_recompute`'s own shape, extended from
1023
+ # reducing a many-side head to a scalar (count/median) to NESTING
1024
+ # it — `ReadModelInterpreter#group_by_target`/`#nest`, reproduced
1025
+ # here in plain Ruby against `history[:instances]` the same way
1026
+ # `eligible_rows` already reproduces the FK-join and `where`
1027
+ # narrowing count/median share. `Value.materialize_unwrapped` is
1028
+ # the SAME call `#project` makes before nesting (a single-field
1029
+ # value object recurses to its bare scalar — a real grouping key
1030
+ # has to BE one) — called here rather than re-derived, so this
1031
+ # oracle cannot drift from what "the group key" means without the
1032
+ # interpreter drifting the identical way.
1033
+ #
1034
+ # Real target: AccountsByKind (`group_by :kind, :number`,
1035
+ # rootless — always generator-eligible with `{}` args).
1036
+ def group_by_matches_recompute(history)
1037
+ bluebook = history.fetch(:bluebook)
1038
+
1039
+ offenders = history.fetch(:queries).filter_map do |asked|
1040
+ next if asked[:error]
1041
+
1042
+ domain, name = asked[:query].to_s.split(".", 2)
1043
+ next unless name && domain == bluebook.name
1044
+
1045
+ model = bluebook.read_model(name)
1046
+ next unless model && model.group_by.any?
1047
+
1048
+ grouped_head = model.aggregate_heads.find { |head| head[:many] }
1049
+ next unless grouped_head
1050
+
1051
+ rows = eligible_rows(bluebook, asked.fetch(:instances_at), domain, model, grouped_head, asked[:args] || {})
1052
+ materialized = rows.map { |state| Runtime::Value.materialize_unwrapped(state) }
1053
+ expected = nest_rows(materialized, model.group_by_fields)
1054
+ actual = asked[:rows]&.first&.dig(grouped_head[:as])
1055
+ next if actual == expected
1056
+
1057
+ "#{asked[:query]} #{asked[:args].inspect} answered a #{grouped_head[:as]} grouping that disagrees " \
1058
+ "with independently nesting group_by #{model.group_by_fields.inspect} over #{rows.length} " \
1059
+ "eligible row(s)"
1060
+ end
1061
+
1062
+ offenders.empty? || offenders.join("; ")
1063
+ end
1064
+
1065
+ # `ReadModelInterpreter#nest`, byte for byte: one level of nesting
1066
+ # per `group_by` field in declared order, leaf is the row with
1067
+ # every grouped field stripped (already spent, as the keys that
1068
+ # reached it).
1069
+ def nest_rows(rows, fields)
1070
+ field, *rest = fields
1071
+ rows.group_by { |row| row[field] }.transform_values do |group|
1072
+ stripped = group.map { |row| row.reject { |key, _| key == field } }
1073
+ rest.empty? ? stripped.first : nest_rows(stripped, rest)
1074
+ end
1075
+ end
1076
+
1077
+ # THE ELIGIBLE ROWS a `count`/`median` head reduces — every
1078
+ # instance of the reduced head's own aggregate, FK-matched against
1079
+ # the report's root reference (if it has one; a rootless report has
1080
+ # none to match) exactly the way `ReadModelInterpreter#reference_fields`
1081
+ # finds the matching attribute, then narrowed by the report's own
1082
+ # `where` clauses via the SAME `InMemory.holds?` the interpreter's
1083
+ # `execute` calls.
1084
+ def eligible_rows(bluebook, instances, domain, model, reduced_head, args)
1085
+ aggregate = bluebook.aggregate(reduced_head[:aggregate])
1086
+ prefix = "#{domain}::#{reduced_head[:aggregate]}#"
1087
+ # `id:` MERGED IN, the same `record.to_h` (`@state.merge(id:
1088
+ # @id)`) every live head row carries — count/median never read
1089
+ # it, but group_by_matches_recompute's own independent nesting
1090
+ # does, the same way ReadModelInterpreter#row(record) = record.
1091
+ # to_h does for the live path it's checking against.
1092
+ rows = instances.filter_map { |key, state| state.merge(id: key.split("#").last) if key.start_with?(prefix) }
1093
+
1094
+ if model.reference_target
1095
+ reference_id = args[model.reference_name].to_s
1096
+ fk_fields = aggregate.attributes.select do |attribute|
1097
+ attribute.reference? && attribute.type.target_name == model.reference_target.to_s
1098
+ end.map(&:name)
1099
+
1100
+ rows = rows.select { |state| fk_fields.any? { |field| state[field].to_s == reference_id } }
1101
+ end
1102
+
1103
+ rows.select do |state|
1104
+ model.wheres.all? do |clause|
1105
+ held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, clause.field))
1106
+ Ports::Query::InMemory.holds?(clause, held, args)
1107
+ end
1108
+ end
1109
+ end
1110
+
1111
+ # `ReadModelInterpreter#median`'s own definition, reproduced byte
1112
+ # for byte: odd count → the true middle value, sorted; even count
1113
+ # → the average of the two middle values, as a Float; empty → nil,
1114
+ # never zero, so a caller cannot mistake "nothing to average" for
1115
+ # "averaged to zero."
1116
+ def recompute_median(rows, field)
1117
+ values = rows.map { |state| Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, field)) }
1118
+ .compact.sort
1119
+ return nil if values.empty?
1120
+
1121
+ middle = values.length / 2
1122
+ values.length.odd? ? values[middle] : (values[middle - 1] + values[middle]) / 2.0
1123
+ end
1124
+
1125
+ # THE STANDARD BATTERY, run over one replayed history — everything
1126
+ # except determinism, which needs to replay TWICE itself and so
1127
+ # takes the steps directly rather than a single history.
1128
+ def check(history)
1129
+ { lifecycle_values_are_declared: lifecycle_values_are_declared(history),
1130
+ saga_advances_follow_declared_handlers: saga_advances_follow_declared_handlers(history),
1131
+ query_answers_match_reference: query_answers_match_reference(history),
1132
+ guard_refusals_are_declared: guard_refusals_are_declared(history),
1133
+ sagas_rehydrate_cleanly: sagas_rehydrate_cleanly(history),
1134
+ fanout_dispatches_once_per_matching_row: fanout_dispatches_once_per_matching_row(history),
1135
+ aggregation_matches_recompute: aggregation_matches_recompute(history),
1136
+ stored_records_satisfy_declared_invariants: stored_records_satisfy_declared_invariants(history),
1137
+ group_by_matches_recompute: group_by_matches_recompute(history),
1138
+ paging_offset_partitions_correctly: paging_offset_partitions_correctly(history),
1139
+ lifecycle_guard_and_given_violations_are_refused: lifecycle_guard_and_given_violations_are_refused(history),
1140
+ authorize_scopes_or_refuses: authorize_scopes_or_refuses(history),
1141
+ dispatch_binding_fidelity: dispatch_binding_fidelity(history),
1142
+ mutations_match_recompute: mutations_match_recompute(history) }
1143
+ end
1144
+ end
1145
+ end
1146
+ end