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,635 @@
1
+ require "fileutils"
2
+ require "tmpdir"
3
+ require_relative "isolated_boot"
4
+ require_relative "../query_specification/common/comparators"
5
+ require_relative "../query_specification/common/where_clause"
6
+ require_relative "../query_specification/field_path"
7
+ require_relative "../ports/query/in_memory"
8
+
9
+ module Hecks
10
+ module Fuzzing
11
+ # A step list, replayed IN-PROCESS against a fresh boot — the same
12
+ # copy-to-tmp-and-reset preamble SequenceGenerator#call uses, and the
13
+ # same observable surface bin/run prints (instances, events,
14
+ # refusals, reactions, sagas, queries), but returned as data rather
15
+ # than JSON on stdout.
16
+ #
17
+ # NOT what SequenceGenerator itself dispatches through while
18
+ # generating — that inline execution feeds the picker's own
19
+ # known_ids tracking and stays exactly as it is. This exists for
20
+ # everything ELSE that needs "given a step list, boot fresh and tell
21
+ # me what happened": bin/fuzz recomputing a shrink candidate's TRUE
22
+ # event count (removing a step changes what the sequence actually
23
+ # produces, so a shrunk candidate cannot reuse the original claim),
24
+ # and the declared-property checks in properties.rb. Both want it
25
+ # in-process — a property or a shrink-candidate check is a question
26
+ # this boot can answer itself, and paying for a subprocess and a
27
+ # second boot to ask it would be ~100x the cost for nothing.
28
+ #
29
+ # Refuses the same way SequenceGenerator's own safe_call does: a
30
+ # DOMAIN_REFUSAL or an EvaluationError is the domain declining a
31
+ # step, recorded and not fatal to the replay. Anything else
32
+ # propagates — a step that breaks the interpreter is a defect, not
33
+ # an observation to fold quietly into the history.
34
+ module Replay
35
+ module_function
36
+
37
+ # THE AD HOC FILTER'S OWN COMPARATOR ROSTER — read directly from
38
+ # QuerySpecification::Common::COMPARATORS (the same nine names
39
+ # Vocabulary::QueryComparator declares), never re-typed. A
40
+ # declared bluebook query never sees an `op:` outside this set —
41
+ # `admits: "Vocabulary::QueryComparator"` refuses one at DECLARE
42
+ # time — but a `"filter"`-shaped query step (below) has no
43
+ # declare-time gate at all, so this method gates it here instead.
44
+ #
45
+ # NOT rust/src/kernel/query_comparators.rs's own ground truth —
46
+ # that hand-maintained Rust enum is missing `none_in_state` (the
47
+ # 9th comparator, added after the enum was written) and has
48
+ # already drifted; do not treat it as authoritative until item #9
49
+ # of the whole-project table-unification survey closes that gap.
50
+ FILTER_COMPARATORS = Hecks::QuerySpecification::Common::COMPARATORS.map(&:to_s).freeze
51
+
52
+ # THE TWO CLASSES `#enforce_givens`/`#enforce_lifecycle_guard`
53
+ # themselves ever raise — see Admissibility's own doc comment,
54
+ # `command_rules/admissibility.rb`. Any OTHER DOMAIN_REFUSAL a step
55
+ # raises (TypeMismatch, EnsuresNotMet, InvariantViolation, ...)
56
+ # proves the guard itself did NOT fire, since it runs first in
57
+ # DISPATCH_ORDER.
58
+ GUARD_REFUSAL_CLASSES = [Runtime::GivenNotMet, Runtime::LifecycleRefused].freeze
59
+
60
+ def call(domain_path, steps)
61
+ # See isolated_boot.rb's own header: resets data/ AND rebinds
62
+ # persistence to Memory, since a Postgres-bound domain's real store
63
+ # lives outside the copied directory and cannot be reached by
64
+ # resetting data/ alone.
65
+ IsolatedBoot.call(domain_path) do |copy|
66
+ runtime = Hecks.boot(copy)
67
+
68
+ refusals = []
69
+ queries = []
70
+ dry_runs = []
71
+ fan_outs = []
72
+ guard_checks = []
73
+ mutation_traces = []
74
+
75
+ # EVERY AGGREGATE A `for_each` COULD EVER QUERY, resolved ONCE —
76
+ # `[domain, aggregate_name]` pairs, gleaned from every loaded
77
+ # bluebook's own fanning-out policies. Empty for every domain
78
+ # with no `for_each` at all (every example this corpus ships
79
+ # today), so the snapshot below costs nothing until a domain
80
+ # actually declares one.
81
+ fan_out_targets = runtime.registry.bluebooks.each_with_object({}) do |(domain, bluebook), targets|
82
+ bluebook.policies.select(&:fans_out?).each do |policy|
83
+ query_domain, aggregate_name, = policy.for_each_route(domain)
84
+ targets[[query_domain, aggregate_name]] ||= runtime.registry.bluebook(query_domain)&.aggregate(aggregate_name)
85
+ end
86
+ end
87
+
88
+ steps.each do |step|
89
+ step = step.transform_keys(&:to_s)
90
+ args = (step["args"] || {}).transform_keys(&:to_sym)
91
+
92
+ if (question = step["query"])
93
+ # THE AD HOC, SINGLE-COMPARATOR FILTER — a "query" step whose
94
+ # own value is a Hash, not a name: `{aggregate:, field:, op:,
95
+ # value:}`, the SAME wire shape kernel/cli.rs's new object-
96
+ # form "query" step reads on the Rust side (that file's own
97
+ # header explains why this shape exists at all: it bypasses
98
+ # the bluebook query DSL entirely, so it needs no generated
99
+ # per-domain codegen to prove for real). Answered here by
100
+ # calling `Ports::Query::InMemory` DIRECTLY — the real
101
+ # production comparator engine, not a second, hand-rewritten
102
+ # copy of it — against the raw repository, never through
103
+ # `runtime.query`, which only ever resolves a NAMED, declared
104
+ # ask.
105
+ if question.is_a?(Hash)
106
+ begin
107
+ rows = run_filter(runtime, question)
108
+ queries << { query: question, rows: rows, instances_at: snapshot_instances(runtime) }
109
+ rescue => e
110
+ refusals << { verb: filter_label(question), error: e.message }
111
+ end
112
+ next
113
+ end
114
+
115
+ begin
116
+ rows = runtime.query(question, **args)
117
+ # THE QUERY ORACLE'S OTHER HALF — the same ask at the same
118
+ # instant, answered by the reference interpreter instead of
119
+ # the bound adapter's native hook. Recorded side by side so
120
+ # Properties.query_answers_match_reference can treat any
121
+ # difference as a finding. Read-model asks (bare domain
122
+ # form, no "::") have no reference twin and record only the
123
+ # one answer.
124
+ reference = question.include?("::") ? runtime.reference_query(question, **args) : nil
125
+ queries << { query: question, args: args, rows: rows, reference_rows: reference,
126
+ instances_at: snapshot_instances(runtime) }
127
+ rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
128
+ queries << { query: question, args: args, error: e.message }
129
+ refusals << { verb: question, error: e.message, kind: e.class.name }
130
+ end
131
+ next
132
+ end
133
+
134
+ # `{"dry_run": verb, "args": …}` — `Dispatcher#dry_run?`: the command
135
+ # evaluated hypothetically, nothing saved or emitted, no reaction.
136
+ # Recorded, never a refusal: a refused dry run is an ANSWER.
137
+ if (hypothetical = step["dry_run"])
138
+ begin
139
+ runtime.dry_run?(hypothetical, **args)
140
+ dry_runs << { verb: hypothetical, ok: true }
141
+ rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
142
+ dry_runs << { verb: hypothetical, ok: false, error: e.message }
143
+ end
144
+ next
145
+ end
146
+
147
+ begin
148
+ # THE FAN-OUT ORACLE'S OWN LOW-WATER MARK — taken before
149
+ # dispatch, so any reaction this ONE step's own announced
150
+ # events produce (`reaction_log` grows in place, the same
151
+ # Array `runtime.reactions` already exposes) can be sliced
152
+ # out after, and matched against an INDEPENDENT recomputation
153
+ # of what a `for_each` policy should have fanned out over —
154
+ # the query oracle's own shape (two engines, compared, never
155
+ # one graded against itself), aimed at fan-out instead of a
156
+ # named ask.
157
+ reaction_mark = runtime.reactions.size
158
+
159
+ # THE SNAPSHOT A `for_each` QUERY WOULD HAVE SEEN — taken
160
+ # BEFORE this step's own dispatch, not after. The real
161
+ # `deliver_for_each` runs its query SYNCHRONOUSLY, inside
162
+ # this SAME dispatch, before this call even returns — so an
163
+ # oracle that re-reads the live repository AFTER `dispatch`
164
+ # answers sees whatever the fan-out's OWN dispatched
165
+ # commands already mutated (an Account a `Review` leg just
166
+ # moved out of "open," say), not what the query actually
167
+ # matched. A measured bug, not a hypothetical one — this
168
+ # exact ordering is what `spec/fuzzing/fan_out_spec.rb`'s
169
+ # own adversarial run against a live `for_each` policy
170
+ # caught the first time this shipped without it.
171
+ fan_out_snapshot = fan_out_targets.each_with_object({}) do |((fdomain, faggregate_name), aggregate), snap|
172
+ next unless aggregate
173
+
174
+ snap[[fdomain, faggregate_name]] =
175
+ runtime.registry.repository(fdomain, aggregate).all.to_h { |record| [record.id, record.state.dup] }
176
+ end
177
+
178
+ # THE GUARD ORACLE'S OWN PRE-DISPATCH READ — same idiom,
179
+ # same placement, same reason as fan_out_snapshot right
180
+ # above: `Admissibility#enforce_givens` (which itself calls
181
+ # `#enforce_lifecycle_guard` when `declaring:` is passed)
182
+ # is called a SECOND time here, independently, against the
183
+ # record exactly as CommandInterpreter#hydrate's own acting
184
+ # branch would find it (`repository.find(id).dup` — the
185
+ # identical three-tier id fallback, Identity.of/.from,
186
+ # reproduced read-only) — BEFORE this step's real dispatch
187
+ # can mutate anything a cross-aggregate given dereferences
188
+ # (`customer.status`). A pure predicate read, side-effect
189
+ # free, so calling it twice changes nothing this step
190
+ # itself observes.
191
+ guard_check = build_guard_check(runtime, step["verb"], args)
192
+
193
+ # THE MUTATION ORACLE'S OWN PRE-DISPATCH READ — same
194
+ # idiom again: an ENTITY-DISPATCHED command's own
195
+ # `append`/`remove`/`multiply`/`clamp` mutations (S17's
196
+ # fixture, spec/fixtures/entity_list_mutations, now a real
197
+ # bootable domain) act on the entity's OWN attributes, so
198
+ # the element addressed by this step's own identity args
199
+ # is snapshotted BEFORE dispatch, materialized to plain
200
+ # data — `nil` for anything out of scope (an aggregate-
201
+ # level command, an entity command with no mutations at
202
+ # all, or one whose identity args don't resolve).
203
+ mutation_trace = build_mutation_trace(runtime, step["verb"], args)
204
+
205
+ # `role:` — an OPTIONAL per-step key, absent on every one of
206
+ # the 231 existing `spec/corpus/*.json` steps (their own
207
+ # unwrapped `runtime.dispatch` call, unchanged, so nothing
208
+ # already pinned changes behavior). Binds the SAME ambient
209
+ # caller `refuse_role_mismatch` reads (`Hecks.as_caller`,
210
+ # `Runtime::Caller.as`) for exactly the one dispatch this
211
+ # step makes, then unbinds — mirrors `Caller.as`'s own
212
+ # `ensure`-restore, so back-to-back steps with different (or
213
+ # no) `role:` never leak into each other.
214
+ result = if step["role"]
215
+ Hecks.as_caller(role: step["role"]) { runtime.dispatch(step["verb"], **args) }
216
+ else
217
+ runtime.dispatch(step["verb"], **args)
218
+ end
219
+
220
+ fan_outs.concat(fan_out_findings(runtime, fan_out_snapshot, result.events, runtime.reactions[reaction_mark..]))
221
+ guard_checks << guard_check.merge(actual_refused: false, actual_kind: nil) if guard_check
222
+ # AFTER — only on SUCCESS ; a refused step mutated nothing,
223
+ # so there is no "after" to compare (and #build_mutation_
224
+ # trace already skipped anything with no mutations to
225
+ # trace in the first place).
226
+ if mutation_trace
227
+ mutation_traces << mutation_trace.merge(after: read_mutation_after(runtime, mutation_trace))
228
+ end
229
+ rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
230
+ # `kind:` — the RAISED CLASS, not re-derived from the message.
231
+ # `GivenNotMet`/`EnsuresNotMet` share their exact wording
232
+ # ("<command> refused — <description>") with FOUR other
233
+ # refusal templates (Vocabulary's own LifecycleRefused/
234
+ # TypeMismatch/Unauthorized entries) — a property that told
235
+ # a guard refusal apart by pattern-matching the string alone
236
+ # would misread every one of those as a guard, or a guard as
237
+ # one of those. The class is unambiguous where the string
238
+ # is not.
239
+ refusals << { verb: step["verb"], error: e.message, kind: e.class.name }
240
+ # ONLY a refusal raised BY THE GUARD ITSELF counts here —
241
+ # measured, not assumed: a step whose args were simply
242
+ # incomplete (AbsentArgument, from normalize_args — which
243
+ # runs BEFORE enforce_givens in DISPATCH_ORDER) never
244
+ # reached the guard at all, and this oracle's own first
245
+ # live run against real generated pizzas data caught
246
+ # exactly that case as a false positive (a malformed-args
247
+ # step the generator deliberately produces, `amount:`
248
+ # dropped entirely) before this comment existed. Whether a
249
+ # refusal from a stage AFTER enforce_givens (TypeMismatch
250
+ # on a mutation, EnsuresNotMet, InvariantViolation) proves
251
+ # the guard passed can't be told apart from a same-shaped
252
+ # refusal from a stage BEFORE it by class alone (TypeMismatch
253
+ # can come from either), so anything that isn't one of the
254
+ # two guard classes is left OUT of guard_checks entirely —
255
+ # inconclusive, not a claimed pass.
256
+ if guard_check && GUARD_REFUSAL_CLASSES.include?(e.class)
257
+ guard_checks << guard_check.merge(actual_refused: true, actual_kind: e.class.name)
258
+ end
259
+ end
260
+ end
261
+
262
+ instances = snapshot_instances(runtime)
263
+
264
+ events = runtime.events.map { |event|
265
+ { name: event.name, aggregate: event.aggregate, id: event.id, payload: event.payload }
266
+ }
267
+
268
+ # THE LIVE PROCESS-MANAGER STORE, materialised to inert data —
269
+ # `{ pm_name => { correlation => { state:, memory: } } }`, the
270
+ # SAME shape SagaInterpreter#checkpoint hands its persistence
271
+ # adapter (state plus a `Value.materialize`d memory, which is
272
+ # exactly what `deep_copy` there serialises). Captured here
273
+ # because Replay returns the history, not the runtime, and the
274
+ # runtime goes out of scope with the boot — so the saga-durability
275
+ # property (Properties.sagas_rehydrate_cleanly) reads this rather
276
+ # than reaching into a store the Memory rebind leaves as the
277
+ # no-op NULL_SAGA_STORE. Materialised, not raw, so the history
278
+ # stays plain data AND the round-trip check sees exactly the
279
+ # bytes a real adapter would have persisted.
280
+ saga_instances = runtime.registry.saga_instances.each_with_object({}) do |(pm_name, conversations), out|
281
+ out[pm_name] = conversations.each_with_object({}) do |(correlation, instance), rows|
282
+ rows[correlation] = { state: instance[:state], memory: Runtime::Value.materialize(instance[:memory]) }
283
+ end
284
+ end
285
+
286
+ # The booted chapter rides along — the boot already happened, and
287
+ # properties.rb's lifecycle/saga checks need the declared IR
288
+ # (state sets, handler graphs) beside the history it produced.
289
+ # Free: no second boot, just the object the first one already
290
+ # built.
291
+ #
292
+ # `bluebook:` (singular) stays the FIRST-loaded chapter — every
293
+ # existing property scopes itself to "only what we have the
294
+ # grammar for" against exactly this one, deliberately (see
295
+ # lifecycle_values_are_declared's own comment). `bluebooks:`
296
+ # (plural) is the FULL map, keyed by domain name — a domain
297
+ # under fuzz commonly composes more than one bluebook (banking
298
+ # alone loads Banking + Governance + Identity), and a refusal
299
+ # or an event can legitimately come from ANY of them, not only
300
+ # whichever one happened to load first. A property that needs
301
+ # to resolve a verb back to its OWN declaring bluebook — not
302
+ # "the" bluebook — reads this instead.
303
+ { instances: instances, events: events, refusals: refusals,
304
+ reactions: runtime.reactions, sagas: runtime.sagas, saga_instances: saga_instances,
305
+ queries: queries, dry_runs: dry_runs, fan_outs: fan_outs, guard_checks: guard_checks,
306
+ mutation_traces: mutation_traces,
307
+ saga_dispatches: runtime.saga_dispatches, policy_dispatches: runtime.policy_dispatches,
308
+ bluebook: runtime.registry.bluebooks.values.first,
309
+ bluebooks: runtime.registry.bluebooks.dup }
310
+ end
311
+ end
312
+
313
+ # THE GUARD ORACLE'S OWN RESOLUTION — "which record, if any, is
314
+ # this step about, and would enforce_givens/enforce_lifecycle_guard
315
+ # have refused it against that record's PRE-DISPATCH state" —
316
+ # reproduced read-only from already-public pieces
317
+ # (Naming.split_verb, registry.bluebook/.aggregate/.command,
318
+ # Runtime::Identity.of/.from, repository.find), the exact same
319
+ # three-tier fallback CommandInterpreter#hydrate's OWN acting
320
+ # branch uses, minus its creating/duplicate-checking logic (a
321
+ # creating command has no pre-existing record to snapshot, and
322
+ # every real target this closes — Debit/CloseAccount/Credit/
323
+ # FreezeAccount — already acts on one).
324
+ #
325
+ # `nil` for anything out of scope: an entity/port verb (a dotted
326
+ # command_name — EntityInterpreter's own enforce_givens call,
327
+ # `parent:`-shaped, is a different call signature this does not
328
+ # reproduce), a creating command, an unresolvable id, or a command
329
+ # that declares neither `givens` nor `from` at all (nothing to
330
+ # check — logging a guaranteed, tautological pass would be noise,
331
+ # not a finding, the same reason `aggregation_matches_recompute`
332
+ # skips a read model with no count/median declared).
333
+ #
334
+ # Never lets a resolution surprise (a malformed verb, a dangling
335
+ # reference) become the step's own real dispatch outcome — this
336
+ # is a SEPARATE, best-effort read, not part of the step's own
337
+ # control flow.
338
+ # THE SAME SHAPE `call`'s own end-of-replay block used to build
339
+ # inline — every persisted record, keyed the way `query_eligible_rows`/
340
+ # `#eligible_rows` (properties.rb) already expect. Now ALSO called
341
+ # once PER QUERY STEP (see `call`, above), not only once at the very
342
+ # end: a query asked at step 1 of a script whose LATER steps go on
343
+ # to create more records was being checked, by every property that
344
+ # independently recomputes "the eligible rows," against the FINAL
345
+ # snapshot — the records that existed AFTER the whole replay, not
346
+ # the ones that existed when the query actually ran. Found live:
347
+ # `Banking.accounts_by_kind`, asked as literally the first step of a
348
+ # 3-step script, correctly answered against zero accounts (none
349
+ # existed yet) while `group_by_matches_recompute`'s own independent
350
+ # recompute claimed "1 eligible row" — the ONE account the script's
351
+ # later two steps went on to create. Each query step now carries
352
+ # its own `instances_at:` snapshot, taken at the moment it ran, so
353
+ # every property that recomputes against "the eligible rows" reads
354
+ # the state as that query actually saw it, not a shared final one.
355
+ def snapshot_instances(runtime)
356
+ instances = {}
357
+ runtime.registry.bluebooks.each do |domain_name, bluebook|
358
+ bluebook.aggregates.each do |aggregate|
359
+ runtime.registry.repository(domain_name, aggregate).all.each do |record|
360
+ instances["#{domain_name}::#{aggregate.name}##{record.id}"] = record.state
361
+ end
362
+ end
363
+ end
364
+ instances
365
+ end
366
+
367
+ def build_guard_check(runtime, verb, args)
368
+ domain_name, aggregate_name, command_name = Naming.split_verb(verb)
369
+ return nil unless command_name && !command_name.include?(".")
370
+
371
+ aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
372
+ command = aggregate&.command(command_name)
373
+ return nil unless aggregate && command && !command.creates?
374
+
375
+ # NOTHING TO CHECK, genuinely — not "nothing THIS reproduces yet".
376
+ # A transition-only guard (no per-command `from:`, no `given`,
377
+ # only an aggregate `lifecycle do transition ... end` block
378
+ # naming this command — `Admit`/`Reject`'s own shape) still
379
+ # counts as something to check now that `admissible_transition`
380
+ # is reproduced below; skipping it here would just move the
381
+ # exact gap that call was added to close one line earlier.
382
+ has_transition = aggregate.lifecycle && aggregate.lifecycle.transitions_for(command.hecks_name).any?
383
+ return nil if command.givens.empty? && !command.from && !has_transition
384
+
385
+ reference_key = command.references.to_s.empty? ? nil : Naming.reference_key(command.references)
386
+ id = Runtime::Identity.of(aggregate, args) ||
387
+ Runtime::Identity.from(aggregate, args, :id) ||
388
+ (reference_key && Runtime::Identity.from(aggregate, args, reference_key))
389
+ return nil unless id
390
+
391
+ record = runtime.registry.repository(domain_name, aggregate).find(id)
392
+ return nil unless record
393
+
394
+ rules = Runtime::CommandRules.new(runtime.registry)
395
+ recomputed_kind = begin
396
+ rules.enforce_givens(record.dup, command, args, domain: domain_name, declaring: aggregate)
397
+
398
+ # A SECOND, SEPARATE DISPATCH_ORDER STEP — `enforce_givens`
399
+ # (just above) only ever checks a per-COMMAND `from:` clause
400
+ # (its own trailing `enforce_lifecycle_guard(declaring, ...)
401
+ # if declaring` call) — the aggregate's own `lifecycle do
402
+ # transition "X" => Y, from: Z end` block is a WHOLLY separate
403
+ # method (`admissible_transition`), called as its own later
404
+ # DISPATCH_ORDER step (`:enforce_givens` then
405
+ # `:admissible_transition` — Vocabulary.symbols
406
+ # ("AggregateDispatchOrder")), not reached from inside
407
+ # `enforce_givens` at all. Missing this call meant a command
408
+ # declared with NO per-command `from:` of its own — every real
409
+ # transition-guarded command in this corpus, `Admit`/`Reject`
410
+ # included — always recomputed "admitted" no matter the
411
+ # record's actual state, because the ONE check that would
412
+ # have refused it was never run. Found live: `Expression::
413
+ # Expression.Admit`, fuzzed against `lib/hecks/grammar`
414
+ # (a domain the property's own hand-verification — Banking,
415
+ # Pizzas — never happened to exercise a transition-guarded,
416
+ # no-per-command-`from:` command against). Called only when
417
+ # `enforce_givens` didn't already refuse, mirroring the real
418
+ # pipeline's own "first refusal wins" order exactly.
419
+ rules.admissible_transition(aggregate, command, record.dup)
420
+ nil
421
+ rescue *GUARD_REFUSAL_CLASSES => e
422
+ e.class.name
423
+ end
424
+
425
+ { verb: verb, domain: domain_name, aggregate: aggregate_name, command: command_name, id: id,
426
+ recomputed_refused: !recomputed_kind.nil?, recomputed_kind: recomputed_kind }
427
+ rescue StandardError
428
+ nil
429
+ end
430
+
431
+ # THE MUTATION ORACLE'S OWN PRE-DISPATCH READ — scoped, on
432
+ # purpose, to ENTITY-DISPATCHED commands only (a dotted
433
+ # command_name): the one place `append`/`remove`/`multiply`/
434
+ # `clamp` are known to act on an entity's OWN attributes
435
+ # (spec/fixtures/entity_list_mutations' own TaggedList — `tags`
436
+ # a value-object list, `count` a VO-typed scalar), never on
437
+ # ANOTHER nested entity list — so this never needs to reproduce
438
+ # `MutationApplier#entity_element`'s own auto-mint/collision logic
439
+ # (item 1's own fix) at all. An aggregate-level command whose OWN
440
+ # mutation appends an ENTITY (`Board.AddList`, `SafeDepositBox.
441
+ # LogVisit`) is a DIFFERENT, already-covered case — item 1's own
442
+ # collision property, not this one.
443
+ #
444
+ # `nil` for anything out of scope: an aggregate-level command, an
445
+ # entity command with no mutations at all, or one whose identity
446
+ # args (parent OR element) don't resolve.
447
+ def build_mutation_trace(runtime, verb, args)
448
+ domain_name, aggregate_name, command_name = Naming.split_verb(verb)
449
+ return nil unless command_name && command_name.include?(".")
450
+
451
+ aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
452
+ return nil unless aggregate
453
+
454
+ entity_name, entity_command_name = command_name.split(".", 2)
455
+ entity = aggregate.entities.find { |candidate| candidate.hecks_name == entity_name }
456
+ command = entity&.command(entity_command_name)
457
+ return nil unless command && command.mutations.any?
458
+
459
+ reference_key = command.references.to_s.empty? ? nil : Naming.reference_key(command.references)
460
+ parent_id = Runtime::Identity.of(aggregate, args) ||
461
+ Runtime::Identity.from(aggregate, args, :id) ||
462
+ (reference_key && Runtime::Identity.from(aggregate, args, reference_key))
463
+ return nil unless parent_id
464
+
465
+ record = runtime.registry.repository(domain_name, aggregate).find(parent_id)
466
+ return nil unless record
467
+
468
+ list_attr = aggregate.attributes.find { |a| a.list? && a.type.to_s == entity.hecks_name }
469
+ return nil unless list_attr
470
+
471
+ wants = entity.identity_paths.map do |path|
472
+ head = path.to_s.split(".").first.to_sym
473
+ raw = args[head]
474
+ return nil if raw.nil?
475
+
476
+ [head, Runtime::Value.for_attribute(aggregate, entity.attribute(head), raw)]
477
+ end
478
+
479
+ element = Array(record.state[list_attr.name]).find { |el| wants.all? { |head, want| el[head] == want } }
480
+ return nil unless element
481
+
482
+ { verb: verb, domain: domain_name, aggregate: aggregate_name, command: command_name,
483
+ parent_id: parent_id, list_attr: list_attr.name, element_wants: wants,
484
+ before: Runtime::Value.materialize(element), args: args }
485
+ rescue StandardError
486
+ nil
487
+ end
488
+
489
+ # THE SAME ELEMENT, RE-LOCATED, AFTER dispatch — by identity, not
490
+ # position (an append could have changed the array's own length
491
+ # or order relative to it). `nil` if it somehow vanished (not
492
+ # expected for any op this fixture declares — none of them
493
+ # remove the acted-on element itself — but a property comparing
494
+ # against `nil` fails loudly rather than crashing this replay).
495
+ def read_mutation_after(runtime, trace)
496
+ aggregate = runtime.registry.bluebook(trace[:domain])&.aggregate(trace[:aggregate])
497
+ return nil unless aggregate
498
+
499
+ record = runtime.registry.repository(trace[:domain], aggregate).find(trace[:parent_id])
500
+ return nil unless record
501
+
502
+ element = Array(record.state[trace[:list_attr]]).find do |el|
503
+ trace[:element_wants].all? { |head, want| el[head] == want }
504
+ end
505
+ element && Runtime::Value.materialize(element)
506
+ rescue StandardError
507
+ nil
508
+ end
509
+
510
+ # THE FAN-OUT ORACLE — one finding per (event, for_each policy) this
511
+ # step's own announced events could have triggered, independent of
512
+ # `PolicyInterpreter#deliver_for_each`: the SAME `where` evaluator
513
+ # every given/ensures already runs through, but the QUERY answered
514
+ # by `Ports::Query::InMemory.holds?` directly against the live
515
+ # repository (`Replay.run_filter`'s own idiom), never by calling
516
+ # `QueryInterpreter` — sharing that call would make this oracle
517
+ # blind to exactly the code the fan-out feature adds.
518
+ #
519
+ # `expected_row_ids` is `nil` when `where` did not hold — no dispatch
520
+ # is the claim, not "dispatched to zero rows," so a property
521
+ # comparing this against the reaction log needs to tell the two
522
+ # apart. Recomputed once per event, not once per policy-and-event,
523
+ # because a `Chapter` de-duplicates on nothing this loop cannot
524
+ # cheaply repeat.
525
+ def fan_out_findings(runtime, snapshot, announced, reactions_since)
526
+ announced.each_with_object([]) do |event, findings|
527
+ # `event.aggregate` is domain-qualified ("Banking::Account" —
528
+ # see command_rules/emission.rb's own Event.new) — the SAME
529
+ # source `PolicyInterpreter#policies_for` reads, split the
530
+ # same two ways: `Naming.demodulise` for the emitting
531
+ # aggregate's bare name, plain `split("::")` for the domain.
532
+ domain = event.aggregate.to_s.split("::").first
533
+ bluebook = runtime.registry.bluebook(domain)
534
+ next unless bluebook
535
+
536
+ emitting = Naming.demodulise(event.aggregate)
537
+
538
+ bluebook.policies.each do |policy|
539
+ next unless policy.fans_out? && policy.event_name == event.name
540
+ next unless policy.event_qualifier.nil? || policy.event_qualifier == emitting
541
+
542
+ findings << fan_out_finding(runtime, snapshot, policy, event, domain, reactions_since)
543
+ end
544
+ end
545
+ end
546
+
547
+ def fan_out_finding(runtime, snapshot, policy, event, domain, reactions_since)
548
+ payload = event.payload.transform_keys(&:to_sym)
549
+ held = policy.where.to_s.empty? ||
550
+ Bluebook::Expression::Evaluator.call(policy.where, {}, payload)
551
+
552
+ expected = held ? expected_fan_out_rows(runtime, snapshot, policy, domain, payload) : nil
553
+
554
+ actual = reactions_since.select { |r| r[:policy] == policy.name && r[:on] == event.name }
555
+ .filter_map { |r| r[:for_row] }
556
+
557
+ { policy: policy.name, on: event.name, expected_row_ids: expected, actual_row_ids: actual }
558
+ end
559
+
560
+ # THE INDEPENDENT RECOMPUTATION — `policy.for_each`'s declared query,
561
+ # answered against the PRE-DISPATCH snapshot (see the snapshot's
562
+ # own comment at its capture site: the real fan-out's query runs
563
+ # synchronously, before its own dispatched commands can mutate
564
+ # anything the query would have matched, so this has to read the
565
+ # same "before" state or it grades the wrong moment). Comparators
566
+ # are `Ports::Query::InMemory.holds?`, never `QueryInterpreter` —
567
+ # sharing that call would make this oracle blind to exactly the
568
+ # code the fan-out feature adds. A `Symbol` where-value binds to
569
+ # the triggering event's own payload (the same binding
570
+ # `OpenForCustomer`'s `customer_id: :customer_id` relies on); a
571
+ # literal is compared as declared.
572
+ def expected_fan_out_rows(runtime, snapshot, policy, domain, payload)
573
+ query_domain, aggregate_name, query_name = policy.for_each_route(domain)
574
+ aggregate = runtime.registry.bluebook(query_domain)&.aggregate(aggregate_name)
575
+ query = aggregate&.query(query_name)
576
+ return [] unless query
577
+
578
+ rows = snapshot[[query_domain, aggregate_name]] || {}
579
+ matched = rows.select do |_id, state|
580
+ query.wheres.all? do |clause|
581
+ held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, clause.field))
582
+ Ports::Query::InMemory.holds?(clause, held, payload)
583
+ end
584
+ end
585
+
586
+ matched.keys.map(&:to_s).sort
587
+ end
588
+
589
+ # Answers ONE ad hoc filter step for real — the mirror image of
590
+ # kernel/cli.rs's own `run_filter`, deliberately calling the exact
591
+ # SAME production module that method's Rust port stands in for
592
+ # (`Ports::Query::InMemory`, lib/hecks/ports/query/in_memory.rb)
593
+ # rather than re-deriving comparator behavior by hand. `field` walks
594
+ # through `QuerySpecification::FieldPath.dig` (the same reading a
595
+ # declared where-clause gets), `comparable`/`holds?` are the same
596
+ # two calls `InMemory.execute` itself makes per candidate record —
597
+ # this is that method's own filter/select step, inlined, because
598
+ # there is no DECLARED `Query` object here to hand `execute` (an ad
599
+ # hoc filter has no `order_by`/`limit`/`offset` at all, so nothing
600
+ # about `execute`'s own ordering/paging logic even applies).
601
+ # Sorted by id ascending regardless — `Ports::Query::Ordering`'s own
602
+ # header explains why an ask with no declared order still needs
603
+ # this tier ("the identity tier is what makes an ask total").
604
+ def run_filter(runtime, filter)
605
+ aggregate_ref = filter["aggregate"].to_s
606
+ field = filter["field"].to_s
607
+ op = filter["op"].to_s
608
+ value = filter["value"]
609
+
610
+ raise "unknown query comparator #{op.inspect}" unless FILTER_COMPARATORS.include?(op)
611
+
612
+ domain_name, aggregate_name = aggregate_ref.split("::", 2)
613
+ aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
614
+ raise "unknown aggregate #{aggregate_ref.inspect}" unless aggregate
615
+
616
+ clause = QuerySpecification::Common::WhereClause.new(field: field, op: op, value: value)
617
+ records = runtime.registry.repository(domain_name, aggregate).all
618
+ matched = records.select do |record|
619
+ held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(record, field))
620
+ Ports::Query::InMemory.holds?(clause, held, {})
621
+ end
622
+
623
+ matched.sort_by { |record| record.id.to_s }.map { |record| { id: record.id }.merge(record.state) }
624
+ end
625
+
626
+ # The `refusals` entry's own "verb" column for a REFUSED ad hoc
627
+ # filter — there is no real verb to report (a filter step carries
628
+ # none), so this builds the SAME descriptive label kernel/cli.rs's
629
+ # own `filter_label` builds from the same three raw fields, tolerant
630
+ # of any of them being missing (Ruby's own nil-to-"" interpolation)
631
+ # the same way that Rust port is.
632
+ def filter_label(filter) = "filter #{filter["aggregate"]}.#{filter["field"]} #{filter["op"]}"
633
+ end
634
+ end
635
+ end