hecks 0.2.0 → 1.0.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 (419) 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 +427 -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 +101 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +179 -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 +145 -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 +88 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +438 -0
  31. data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
  32. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  33. data/lib/hecks/adapters/driven/prism.rb +80 -0
  34. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  35. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  36. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  37. data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
  38. data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
  39. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
  40. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  41. data/lib/hecks/adapters/driven/sqlite.rb +306 -0
  42. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  43. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  44. data/lib/hecks/adapters/driven.rb +54 -0
  45. data/lib/hecks/adapters.rb +6 -0
  46. data/lib/hecks/behaviors/dsl.rb +125 -0
  47. data/lib/hecks/behaviors/expectations.rb +340 -0
  48. data/lib/hecks/behaviors/ir.rb +31 -0
  49. data/lib/hecks/behaviors/rspec.rb +42 -0
  50. data/lib/hecks/behaviors/runner.rb +96 -0
  51. data/lib/hecks/behaviors.rb +25 -0
  52. data/lib/hecks/bluebook/aggregate.rb +108 -0
  53. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  54. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  55. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  56. data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
  57. data/lib/hecks/bluebook/assembly/marks.rb +228 -0
  58. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  59. data/lib/hecks/bluebook/assembly.rb +91 -0
  60. data/lib/hecks/bluebook/attribute.rb +96 -0
  61. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  62. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  63. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  64. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  65. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  66. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  67. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  68. data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
  69. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  70. data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
  71. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  73. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  75. data/lib/hecks/bluebook/chapter.rb +78 -0
  76. data/lib/hecks/bluebook/command.rb +124 -0
  77. data/lib/hecks/bluebook/domain_port.rb +102 -0
  78. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
  80. data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
  81. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  82. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
  83. data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
  84. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  85. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  86. data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
  87. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  88. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  89. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  90. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  91. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  92. data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
  93. data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
  94. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  95. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
  96. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  97. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  98. data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
  99. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  100. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  101. data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
  102. data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
  103. data/lib/hecks/bluebook/dsl.rb +45 -0
  104. data/lib/hecks/bluebook/entity.rb +103 -0
  105. data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
  106. data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
  107. data/lib/hecks/bluebook/expression/projection.json +218 -0
  108. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  109. data/lib/hecks/bluebook/expression/resolver.rb +781 -0
  110. data/lib/hecks/bluebook/expression.rb +13 -0
  111. data/lib/hecks/bluebook/hexagon.rb +60 -0
  112. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  113. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  114. data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
  115. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  116. data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
  117. data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
  118. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
  119. data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
  120. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  121. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  122. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  123. data/lib/hecks/bluebook/meta_validator.rb +542 -0
  124. data/lib/hecks/bluebook/model_check.rb +445 -0
  125. data/lib/hecks/bluebook/pattern_subset.rb +184 -0
  126. data/lib/hecks/bluebook/policy.rb +41 -0
  127. data/lib/hecks/bluebook/process_manager.rb +128 -0
  128. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  129. data/lib/hecks/bluebook/project_loader.rb +40 -0
  130. data/lib/hecks/bluebook/project_register.rb +82 -0
  131. data/lib/hecks/bluebook/query.rb +61 -0
  132. data/lib/hecks/bluebook/read_model.rb +109 -0
  133. data/lib/hecks/bluebook/reference.rb +74 -0
  134. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  135. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  136. data/lib/hecks/bluebook/translation.rb +92 -0
  137. data/lib/hecks/bluebook/value_object.rb +66 -0
  138. data/lib/hecks/bluebook.rb +74 -0
  139. data/lib/hecks/codemod.rb +342 -0
  140. data/lib/hecks/construct.rb +71 -0
  141. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  142. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  143. data/lib/hecks/deploy/oidc.json +18 -0
  144. data/lib/hecks/doc/reference.rb +410 -0
  145. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  146. data/lib/hecks/facade/cli_door.rb +119 -0
  147. data/lib/hecks/facade/cli_runner.rb +190 -0
  148. data/lib/hecks/facade/command_request.rb +105 -0
  149. data/lib/hecks/facade/handle.rb +173 -0
  150. data/lib/hecks/facade/json_door.rb +166 -0
  151. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  152. data/lib/hecks/facade/surface/chapter.rb +107 -0
  153. data/lib/hecks/facade/surface.rb +48 -0
  154. data/lib/hecks/facade.rb +44 -0
  155. data/lib/hecks/forms/app.rb +341 -0
  156. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  157. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  158. data/lib/hecks/forms/field_renderer.rb +177 -0
  159. data/lib/hecks/forms/field_shape.rb +232 -0
  160. data/lib/hecks/forms/html.rb +84 -0
  161. data/lib/hecks/forms/index_renderer.rb +35 -0
  162. data/lib/hecks/forms/page.rb +157 -0
  163. data/lib/hecks/forms/params.rb +161 -0
  164. data/lib/hecks/forms/port_argument.rb +46 -0
  165. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  166. data/lib/hecks/forms/record_renderer.rb +119 -0
  167. data/lib/hecks/forms/record_table.rb +68 -0
  168. data/lib/hecks/forms/reference_options.rb +30 -0
  169. data/lib/hecks/forms/value_object_shape.rb +46 -0
  170. data/lib/hecks/forms.rb +54 -0
  171. data/lib/hecks/fqn.rb +94 -0
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  175. data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
  176. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  177. data/lib/hecks/framework/oidc.json +39 -0
  178. data/lib/hecks/framework.rb +90 -0
  179. data/lib/hecks/freezer.rb +67 -0
  180. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  181. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  182. data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
  183. data/lib/hecks/fuzzing/properties.rb +1192 -0
  184. data/lib/hecks/fuzzing/replay.rb +668 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
  189. data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
  190. data/lib/hecks/fuzzing/value_generator.rb +200 -0
  191. data/lib/hecks/fuzzing.rb +12 -0
  192. data/lib/hecks/grammar/evolve.rb +324 -0
  193. data/lib/hecks/grammar/expression.bluebook +420 -0
  194. data/lib/hecks/grammar/expression_operators.json +1648 -0
  195. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  196. data/lib/hecks/grammar/oidc.json +69 -0
  197. data/lib/hecks/grammar/translation.bluebook +203 -0
  198. data/lib/hecks/grammar.rb +178 -0
  199. data/lib/hecks/ir.rb +126 -0
  200. data/lib/hecks/language/adapter.bluebook +116 -0
  201. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  202. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  203. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  204. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  205. data/lib/hecks/language/bluebook/command.bluebook +471 -0
  206. data/lib/hecks/language/bluebook/entity.bluebook +392 -0
  207. data/lib/hecks/language/bluebook/policy.bluebook +189 -0
  208. data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
  209. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  210. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  211. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  212. data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
  213. data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
  214. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  215. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  216. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  217. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  218. data/lib/hecks/language/oidc.json +333 -0
  219. data/lib/hecks/language/port.bluebook +120 -0
  220. data/lib/hecks/language/translation/translation.bluebook +110 -0
  221. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  222. data/lib/hecks/language/world/wiring.bluebook +62 -0
  223. data/lib/hecks/language/world/world.bluebook +84 -0
  224. data/lib/hecks/literal.rb +125 -0
  225. data/lib/hecks/naming.rb +174 -0
  226. data/lib/hecks/ports/access_control.port +9 -0
  227. data/lib/hecks/ports/access_control.rb +62 -0
  228. data/lib/hecks/ports/agent/answers.rb +104 -0
  229. data/lib/hecks/ports/agent.port +8 -0
  230. data/lib/hecks/ports/agent.rb +167 -0
  231. data/lib/hecks/ports/authentication.port +6 -0
  232. data/lib/hecks/ports/authentication.rb +50 -0
  233. data/lib/hecks/ports/authorization.port +7 -0
  234. data/lib/hecks/ports/authorization.rb +62 -0
  235. data/lib/hecks/ports/clock.port +5 -0
  236. data/lib/hecks/ports/clock.rb +62 -0
  237. data/lib/hecks/ports/extraction.port +5 -0
  238. data/lib/hecks/ports/extraction.rb +37 -0
  239. data/lib/hecks/ports/identity_assignment.port +5 -0
  240. data/lib/hecks/ports/identity_assignment.rb +45 -0
  241. data/lib/hecks/ports/identity_generation.port +5 -0
  242. data/lib/hecks/ports/identity_generation.rb +49 -0
  243. data/lib/hecks/ports/identity_resolution.port +5 -0
  244. data/lib/hecks/ports/identity_resolution.rb +40 -0
  245. data/lib/hecks/ports/loading.port +4 -0
  246. data/lib/hecks/ports/loading.rb +13 -0
  247. data/lib/hecks/ports/persistence/append_only.rb +138 -0
  248. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  249. data/lib/hecks/ports/persistence/execution.rb +23 -0
  250. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  251. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  252. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
  253. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
  254. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
  255. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
  256. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
  257. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
  258. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
  259. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
  260. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
  261. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
  262. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
  263. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
  264. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
  265. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
  266. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
  267. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
  268. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  269. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
  270. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
  271. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
  272. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
  273. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
  274. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
  275. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
  276. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
  277. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
  278. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
  279. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
  280. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
  281. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
  282. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
  283. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
  284. data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
  285. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  286. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  287. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  288. data/lib/hecks/ports/persistence.port +4 -0
  289. data/lib/hecks/ports/persistence.rb +39 -0
  290. data/lib/hecks/ports/projection.port +4 -0
  291. data/lib/hecks/ports/projection.rb +96 -0
  292. data/lib/hecks/ports/query/in_memory.rb +60 -0
  293. data/lib/hecks/ports/query/ordering.rb +41 -0
  294. data/lib/hecks/ports/query.rb +36 -0
  295. data/lib/hecks/ports.rb +27 -0
  296. data/lib/hecks/projections/diagrams.rb +642 -0
  297. data/lib/hecks/projections/ir.rb +18 -0
  298. data/lib/hecks/projections/model/deviations.rb +98 -0
  299. data/lib/hecks/projections/model.rb +145 -0
  300. data/lib/hecks/projections/oidc.rb +110 -0
  301. data/lib/hecks/projections/parser_table.rb +159 -0
  302. data/lib/hecks/projections/reference.rb +38 -0
  303. data/lib/hecks/projections/shape.rb +44 -0
  304. data/lib/hecks/projections/statements.rb +110 -0
  305. data/lib/hecks/projections/vocabulary.rb +100 -0
  306. data/lib/hecks/projections.rb +32 -0
  307. data/lib/hecks/projector/cli_projector.rb +446 -0
  308. data/lib/hecks/projector/docs_projector.rb +321 -0
  309. data/lib/hecks/projector/exporter.rb +158 -0
  310. data/lib/hecks/projector/ir_projector.rb +18 -0
  311. data/lib/hecks/projector/narrate_projector.rb +243 -0
  312. data/lib/hecks/projector/target.rb +97 -0
  313. data/lib/hecks/projector.rb +186 -0
  314. data/lib/hecks/query_ir.rb +411 -0
  315. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  316. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  317. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  318. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  319. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  320. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  321. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  322. data/lib/hecks/query_specification/common/null_policy.rb +92 -0
  323. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  324. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  325. data/lib/hecks/query_specification/common/options.rb +32 -0
  326. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  327. data/lib/hecks/query_specification/common/specification.rb +10 -0
  328. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  329. data/lib/hecks/query_specification/field_path.rb +107 -0
  330. data/lib/hecks/query_specification/hop_path.rb +132 -0
  331. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  332. data/lib/hecks/query_specification.rb +14 -0
  333. data/lib/hecks/rendering.rb +48 -0
  334. data/lib/hecks/router/namespace_installer.rb +157 -0
  335. data/lib/hecks/router.rb +70 -0
  336. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  337. data/lib/hecks/runtime/boot_gates.rb +41 -0
  338. data/lib/hecks/runtime/caller.rb +66 -0
  339. data/lib/hecks/runtime/capability_graph.rb +44 -0
  340. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
  341. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
  342. data/lib/hecks/runtime/command_interpreter.rb +497 -0
  343. data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
  344. data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
  345. data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
  346. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  347. data/lib/hecks/runtime/command_rules/references.rb +189 -0
  348. data/lib/hecks/runtime/command_rules.rb +28 -0
  349. data/lib/hecks/runtime/dependency_planning.rb +245 -0
  350. data/lib/hecks/runtime/dispatcher.rb +301 -0
  351. data/lib/hecks/runtime/entity_element.rb +253 -0
  352. data/lib/hecks/runtime/entity_interpreter.rb +299 -0
  353. data/lib/hecks/runtime/errors.rb +122 -0
  354. data/lib/hecks/runtime/event.rb +51 -0
  355. data/lib/hecks/runtime/identity.rb +156 -0
  356. data/lib/hecks/runtime/instance.rb +175 -0
  357. data/lib/hecks/runtime/interpreting.rb +90 -0
  358. data/lib/hecks/runtime/loader.rb +184 -0
  359. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  360. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  361. data/lib/hecks/runtime/query_interpreter.rb +286 -0
  362. data/lib/hecks/runtime/reaction_invocation.rb +253 -0
  363. data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
  364. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  365. data/lib/hecks/runtime/reference_hop.rb +99 -0
  366. data/lib/hecks/runtime/refusal_wording.rb +126 -0
  367. data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
  368. data/lib/hecks/runtime/registry/verification.rb +246 -0
  369. data/lib/hecks/runtime/registry.rb +279 -0
  370. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  371. data/lib/hecks/runtime/routing.rb +96 -0
  372. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  373. data/lib/hecks/runtime/saga_interpreter.rb +503 -0
  374. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  375. data/lib/hecks/runtime/tenant_check.rb +84 -0
  376. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  377. data/lib/hecks/runtime/value/admission.rb +130 -0
  378. data/lib/hecks/runtime/value/coercion.rb +565 -0
  379. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  380. data/lib/hecks/runtime/value.rb +125 -0
  381. data/lib/hecks/runtime.rb +107 -0
  382. data/lib/hecks/storehouse.rb +632 -0
  383. data/lib/hecks/version.rb +16 -0
  384. data/lib/hecks/vocabulary.rb +218 -0
  385. data/lib/hecks.rb +123 -6
  386. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  387. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  388. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  389. metadata +412 -222
  390. data/bin/hecks +0 -7
  391. data/bin/hecks-package +0 -65
  392. data/bin/hecks_console +0 -12
  393. data/bin/hecks_serverless +0 -6
  394. data/lib/cli/build.rb +0 -14
  395. data/lib/cli/command_runner.rb +0 -28
  396. data/lib/cli/console.rb +0 -10
  397. data/lib/cli/generate.rb +0 -37
  398. data/lib/cli/hecks-cli.rb +0 -27
  399. data/lib/cli/test.rb +0 -57
  400. data/lib/console/commands.rb +0 -8
  401. data/lib/console/hecks-console.rb +0 -1
  402. data/lib/packager/README.md +0 -0
  403. data/lib/packager/app_runner.rb +0 -21
  404. data/lib/packager/args.rb +0 -26
  405. data/lib/packager/compatibility/fixnum.rb +0 -6
  406. data/lib/packager/hecks.rb +0 -39
  407. data/lib/packager/query_runner.rb +0 -21
  408. data/lib/packager/resources/Dockerfile +0 -11
  409. data/lib/packager/resources/app_binary +0 -7
  410. data/lib/packager/resources/bundle_config +0 -3
  411. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  412. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  413. data/lib/serverless/Domain +0 -32
  414. data/lib/serverless/cli.rb +0 -75
  415. data/lib/serverless/resources/command_name.js +0 -5
  416. data/lib/serverless/resources/environment.js +0 -7
  417. data/lib/serverless/resources/handler.js.tt +0 -28
  418. data/lib/serverless/resources/run_binary.js +0 -22
  419. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,668 @@
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, adapter: :memory)
61
+ # See isolated_boot.rb's own header: resets data/ AND rebinds
62
+ # persistence to the chosen adapter (Memory by default), since a
63
+ # Postgres-bound domain's real store lives outside the copied
64
+ # directory and cannot be reached by resetting data/ alone.
65
+ IsolatedBoot.call(domain_path, adapter: adapter) 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
+ # THE QUERY ORACLE — TWO INDEPENDENT ENGINES, EACH RUN AND
116
+ # CAUGHT ON ITS OWN, never a single shared `begin`/`rescue`
117
+ # wrapping both calls. A shared begin/rescue meant `runtime.
118
+ # query` raising (native refuses) short-circuited BEFORE
119
+ # `runtime.reference_query` ever ran at all — the entry
120
+ # recorded only `error:`, with no `reference_rows` and no
121
+ # record of what the reference interpreter would have
122
+ # answered — and `runtime.reference_query` raising instead
123
+ # (reference refuses, native already succeeded) landed in the
124
+ # SAME rescue, discarding the native `rows` this begin block
125
+ # had already computed and recording the whole ask as an
126
+ # ordinary refusal. Either way, "one engine refused and the
127
+ # other did not" — a real divergence, exactly the shape a
128
+ # differential oracle exists to catch — read as "refused,
129
+ # nothing to compare" and vanished. Each engine's own
130
+ # success/failure is captured here independently instead, so
131
+ # `Properties.query_answers_match_reference` can tell "both
132
+ # refused" (fine) apart from "one refused, the other did not"
133
+ # (a finding) rather than having every refusal-shaped run
134
+ # skipped uniformly.
135
+ native_rows = native_error = nil
136
+ begin
137
+ native_rows = runtime.query(question, **args)
138
+ rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
139
+ native_error = e
140
+ end
141
+
142
+ # Read-model asks (bare domain form, no "::") have no
143
+ # reference twin at all — never attempted, not "attempted and
144
+ # agreed."
145
+ has_reference = question.include?("::")
146
+ reference_rows = reference_error = nil
147
+ if has_reference
148
+ begin
149
+ reference_rows = runtime.reference_query(question, **args)
150
+ rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
151
+ reference_error = e
152
+ end
153
+ end
154
+
155
+ entry = { query: question, args: args, rows: native_rows, instances_at: snapshot_instances(runtime) }
156
+ entry[:error] = native_error.message if native_error
157
+ if has_reference
158
+ entry[:reference_rows] = reference_rows
159
+ entry[:reference_error] = reference_error.message if reference_error
160
+ end
161
+ queries << entry
162
+
163
+ refusals << { verb: question, error: native_error.message, kind: native_error.class.name } if native_error
164
+ next
165
+ end
166
+
167
+ # `{"dry_run": verb, "args": …}` — `Dispatcher#dry_run?`: the command
168
+ # evaluated hypothetically, nothing saved or emitted, no reaction.
169
+ # Recorded, never a refusal: a refused dry run is an ANSWER.
170
+ if (hypothetical = step["dry_run"])
171
+ begin
172
+ runtime.dry_run?(hypothetical, **args)
173
+ dry_runs << { verb: hypothetical, ok: true }
174
+ rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
175
+ dry_runs << { verb: hypothetical, ok: false, error: e.message }
176
+ end
177
+ next
178
+ end
179
+
180
+ begin
181
+ # THE FAN-OUT ORACLE'S OWN LOW-WATER MARK — taken before
182
+ # dispatch, so any reaction this ONE step's own announced
183
+ # events produce (`reaction_log` grows in place, the same
184
+ # Array `runtime.reactions` already exposes) can be sliced
185
+ # out after, and matched against an INDEPENDENT recomputation
186
+ # of what a `for_each` policy should have fanned out over —
187
+ # the query oracle's own shape (two engines, compared, never
188
+ # one graded against itself), aimed at fan-out instead of a
189
+ # named ask.
190
+ reaction_mark = runtime.reactions.size
191
+
192
+ # THE SNAPSHOT A `for_each` QUERY WOULD HAVE SEEN — taken
193
+ # BEFORE this step's own dispatch, not after. The real
194
+ # `deliver_for_each` runs its query SYNCHRONOUSLY, inside
195
+ # this SAME dispatch, before this call even returns — so an
196
+ # oracle that re-reads the live repository AFTER `dispatch`
197
+ # answers sees whatever the fan-out's OWN dispatched
198
+ # commands already mutated (an Account a `Review` leg just
199
+ # moved out of "open," say), not what the query actually
200
+ # matched. A measured bug, not a hypothetical one — this
201
+ # exact ordering is what `spec/fuzzing/fan_out_spec.rb`'s
202
+ # own adversarial run against a live `for_each` policy
203
+ # caught the first time this shipped without it.
204
+ fan_out_snapshot = fan_out_targets.each_with_object({}) do |((fdomain, faggregate_name), aggregate), snap|
205
+ next unless aggregate
206
+
207
+ snap[[fdomain, faggregate_name]] =
208
+ runtime.registry.repository(fdomain, aggregate).all.to_h { |record| [record.id, record.state.dup] }
209
+ end
210
+
211
+ # THE GUARD ORACLE'S OWN PRE-DISPATCH READ — same idiom,
212
+ # same placement, same reason as fan_out_snapshot right
213
+ # above: `Admissibility#enforce_givens` (which itself calls
214
+ # `#enforce_lifecycle_guard` when `declaring:` is passed)
215
+ # is called a SECOND time here, independently, against the
216
+ # record exactly as CommandInterpreter#hydrate's own acting
217
+ # branch would find it (`repository.find(id).dup` — the
218
+ # identical three-tier id fallback, Identity.of/.from,
219
+ # reproduced read-only) — BEFORE this step's real dispatch
220
+ # can mutate anything a cross-aggregate given dereferences
221
+ # (`customer.status`). A pure predicate read, side-effect
222
+ # free, so calling it twice changes nothing this step
223
+ # itself observes.
224
+ guard_check = build_guard_check(runtime, step["verb"], args)
225
+
226
+ # THE MUTATION ORACLE'S OWN PRE-DISPATCH READ — same
227
+ # idiom again: an ENTITY-DISPATCHED command's own
228
+ # `append`/`remove`/`multiply`/`clamp` mutations (S17's
229
+ # fixture, spec/fixtures/entity_list_mutations, now a real
230
+ # bootable domain) act on the entity's OWN attributes, so
231
+ # the element addressed by this step's own identity args
232
+ # is snapshotted BEFORE dispatch, materialized to plain
233
+ # data — `nil` for anything out of scope (an aggregate-
234
+ # level command, an entity command with no mutations at
235
+ # all, or one whose identity args don't resolve).
236
+ mutation_trace = build_mutation_trace(runtime, step["verb"], args)
237
+
238
+ # `role:` — an OPTIONAL per-step key, absent on every one of
239
+ # the 231 existing `spec/corpus/*.json` steps (their own
240
+ # unwrapped `runtime.dispatch` call, unchanged, so nothing
241
+ # already pinned changes behavior). Binds the SAME ambient
242
+ # caller `refuse_role_mismatch` reads (`Hecks.as_caller`,
243
+ # `Runtime::Caller.as`) for exactly the one dispatch this
244
+ # step makes, then unbinds — mirrors `Caller.as`'s own
245
+ # `ensure`-restore, so back-to-back steps with different (or
246
+ # no) `role:` never leak into each other.
247
+ result = if step["role"]
248
+ Hecks.as_caller(role: step["role"]) { runtime.dispatch(step["verb"], **args) }
249
+ else
250
+ runtime.dispatch(step["verb"], **args)
251
+ end
252
+
253
+ fan_outs.concat(fan_out_findings(runtime, fan_out_snapshot, result.events, runtime.reactions[reaction_mark..]))
254
+ guard_checks << guard_check.merge(actual_refused: false, actual_kind: nil) if guard_check
255
+ # AFTER — only on SUCCESS ; a refused step mutated nothing,
256
+ # so there is no "after" to compare (and #build_mutation_
257
+ # trace already skipped anything with no mutations to
258
+ # trace in the first place).
259
+ if mutation_trace
260
+ mutation_traces << mutation_trace.merge(after: read_mutation_after(runtime, mutation_trace))
261
+ end
262
+ rescue *Runtime::DOMAIN_REFUSALS, Bluebook::Expression::EvaluationError => e
263
+ # `kind:` — the RAISED CLASS, not re-derived from the message.
264
+ # `GivenNotMet`/`EnsuresNotMet` share their exact wording
265
+ # ("<command> refused — <description>") with FOUR other
266
+ # refusal templates (Vocabulary's own LifecycleRefused/
267
+ # TypeMismatch/Unauthorized entries) — a property that told
268
+ # a guard refusal apart by pattern-matching the string alone
269
+ # would misread every one of those as a guard, or a guard as
270
+ # one of those. The class is unambiguous where the string
271
+ # is not.
272
+ refusals << { verb: step["verb"], error: e.message, kind: e.class.name }
273
+ # ONLY a refusal raised BY THE GUARD ITSELF counts here —
274
+ # measured, not assumed: a step whose args were simply
275
+ # incomplete (AbsentArgument, from normalize_args — which
276
+ # runs BEFORE enforce_givens in DISPATCH_ORDER) never
277
+ # reached the guard at all, and this oracle's own first
278
+ # live run against real generated pizzas data caught
279
+ # exactly that case as a false positive (a malformed-args
280
+ # step the generator deliberately produces, `amount:`
281
+ # dropped entirely) before this comment existed. Whether a
282
+ # refusal from a stage AFTER enforce_givens (TypeMismatch
283
+ # on a mutation, EnsuresNotMet, InvariantViolation) proves
284
+ # the guard passed can't be told apart from a same-shaped
285
+ # refusal from a stage BEFORE it by class alone (TypeMismatch
286
+ # can come from either), so anything that isn't one of the
287
+ # two guard classes is left OUT of guard_checks entirely —
288
+ # inconclusive, not a claimed pass.
289
+ if guard_check && GUARD_REFUSAL_CLASSES.include?(e.class)
290
+ guard_checks << guard_check.merge(actual_refused: true, actual_kind: e.class.name)
291
+ end
292
+ end
293
+ end
294
+
295
+ instances = snapshot_instances(runtime)
296
+
297
+ events = runtime.events.map { |event|
298
+ { name: event.name, aggregate: event.aggregate, id: event.id, payload: event.payload }
299
+ }
300
+
301
+ # THE LIVE PROCESS-MANAGER STORE, materialised to inert data —
302
+ # `{ pm_name => { correlation => { state:, memory: } } }`, the
303
+ # SAME shape SagaInterpreter#checkpoint hands its persistence
304
+ # adapter (state plus a `Value.materialize`d memory, which is
305
+ # exactly what `deep_copy` there serialises). Captured here
306
+ # because Replay returns the history, not the runtime, and the
307
+ # runtime goes out of scope with the boot — so the saga-durability
308
+ # property (Properties.sagas_rehydrate_cleanly) reads this rather
309
+ # than reaching into a store the Memory rebind leaves as the
310
+ # no-op NULL_SAGA_STORE. Materialised, not raw, so the history
311
+ # stays plain data AND the round-trip check sees exactly the
312
+ # bytes a real adapter would have persisted.
313
+ saga_instances = runtime.registry.saga_instances.each_with_object({}) do |(pm_name, conversations), out|
314
+ out[pm_name] = conversations.each_with_object({}) do |(correlation, instance), rows|
315
+ rows[correlation] = { state: instance[:state], memory: Runtime::Value.materialize(instance[:memory]) }
316
+ end
317
+ end
318
+
319
+ # The booted chapter rides along — the boot already happened, and
320
+ # properties.rb's lifecycle/saga checks need the declared IR
321
+ # (state sets, handler graphs) beside the history it produced.
322
+ # Free: no second boot, just the object the first one already
323
+ # built.
324
+ #
325
+ # `bluebook:` (singular) stays the FIRST-loaded chapter — every
326
+ # existing property scopes itself to "only what we have the
327
+ # grammar for" against exactly this one, deliberately (see
328
+ # lifecycle_values_are_declared's own comment). `bluebooks:`
329
+ # (plural) is the FULL map, keyed by domain name — a domain
330
+ # under fuzz commonly composes more than one bluebook (banking
331
+ # alone loads Banking + Governance + Identity), and a refusal
332
+ # or an event can legitimately come from ANY of them, not only
333
+ # whichever one happened to load first. A property that needs
334
+ # to resolve a verb back to its OWN declaring bluebook — not
335
+ # "the" bluebook — reads this instead.
336
+ { instances: instances, events: events, refusals: refusals,
337
+ reactions: runtime.reactions, sagas: runtime.sagas, saga_instances: saga_instances,
338
+ queries: queries, dry_runs: dry_runs, fan_outs: fan_outs, guard_checks: guard_checks,
339
+ mutation_traces: mutation_traces,
340
+ saga_dispatches: runtime.saga_dispatches, policy_dispatches: runtime.policy_dispatches,
341
+ bluebook: runtime.registry.bluebooks.values.first,
342
+ bluebooks: runtime.registry.bluebooks.dup }
343
+ end
344
+ end
345
+
346
+ # THE GUARD ORACLE'S OWN RESOLUTION — "which record, if any, is
347
+ # this step about, and would enforce_givens/enforce_lifecycle_guard
348
+ # have refused it against that record's PRE-DISPATCH state" —
349
+ # reproduced read-only from already-public pieces
350
+ # (Naming.split_verb, registry.bluebook/.aggregate/.command,
351
+ # Runtime::Identity.of/.from, repository.find), the exact same
352
+ # three-tier fallback CommandInterpreter#hydrate's OWN acting
353
+ # branch uses, minus its creating/duplicate-checking logic (a
354
+ # creating command has no pre-existing record to snapshot, and
355
+ # every real target this closes — Debit/CloseAccount/Credit/
356
+ # FreezeAccount — already acts on one).
357
+ #
358
+ # `nil` for anything out of scope: an entity/port verb (a dotted
359
+ # command_name — EntityInterpreter's own enforce_givens call,
360
+ # `parent:`-shaped, is a different call signature this does not
361
+ # reproduce), a creating command, an unresolvable id, or a command
362
+ # that declares neither `givens` nor `from` at all (nothing to
363
+ # check — logging a guaranteed, tautological pass would be noise,
364
+ # not a finding, the same reason `aggregation_matches_recompute`
365
+ # skips a read model with no count/median declared).
366
+ #
367
+ # Never lets a resolution surprise (a malformed verb, a dangling
368
+ # reference) become the step's own real dispatch outcome — this
369
+ # is a SEPARATE, best-effort read, not part of the step's own
370
+ # control flow.
371
+ # THE SAME SHAPE `call`'s own end-of-replay block used to build
372
+ # inline — every persisted record, keyed the way `query_eligible_rows`/
373
+ # `#eligible_rows` (properties.rb) already expect. Now ALSO called
374
+ # once PER QUERY STEP (see `call`, above), not only once at the very
375
+ # end: a query asked at step 1 of a script whose LATER steps go on
376
+ # to create more records was being checked, by every property that
377
+ # independently recomputes "the eligible rows," against the FINAL
378
+ # snapshot — the records that existed AFTER the whole replay, not
379
+ # the ones that existed when the query actually ran. Found live:
380
+ # `Banking.accounts_by_kind`, asked as literally the first step of a
381
+ # 3-step script, correctly answered against zero accounts (none
382
+ # existed yet) while `group_by_matches_recompute`'s own independent
383
+ # recompute claimed "1 eligible row" — the ONE account the script's
384
+ # later two steps went on to create. Each query step now carries
385
+ # its own `instances_at:` snapshot, taken at the moment it ran, so
386
+ # every property that recomputes against "the eligible rows" reads
387
+ # the state as that query actually saw it, not a shared final one.
388
+ def snapshot_instances(runtime)
389
+ instances = {}
390
+ runtime.registry.bluebooks.each do |domain_name, bluebook|
391
+ bluebook.aggregates.each do |aggregate|
392
+ runtime.registry.repository(domain_name, aggregate).all.each do |record|
393
+ instances["#{domain_name}::#{aggregate.name}##{record.id}"] = record.state
394
+ end
395
+ end
396
+ end
397
+ instances
398
+ end
399
+
400
+ def build_guard_check(runtime, verb, args)
401
+ domain_name, aggregate_name, command_name = Naming.split_verb(verb)
402
+ return nil unless command_name && !command_name.include?(".")
403
+
404
+ aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
405
+ command = aggregate&.command(command_name)
406
+ return nil unless aggregate && command && !command.creates?
407
+
408
+ # NOTHING TO CHECK, genuinely — not "nothing THIS reproduces yet".
409
+ # A transition-only guard (no per-command `from:`, no `given`,
410
+ # only an aggregate `lifecycle do transition ... end` block
411
+ # naming this command — `Admit`/`Reject`'s own shape) still
412
+ # counts as something to check now that `admissible_transition`
413
+ # is reproduced below; skipping it here would just move the
414
+ # exact gap that call was added to close one line earlier.
415
+ has_transition = aggregate.lifecycle && aggregate.lifecycle.transitions_for(command.hecks_name).any?
416
+ return nil if command.givens.empty? && !command.from && !has_transition
417
+
418
+ reference_key = command.references.to_s.empty? ? nil : Naming.reference_key(command.references)
419
+ id = Runtime::Identity.of(aggregate, args) ||
420
+ Runtime::Identity.from(aggregate, args, :id) ||
421
+ (reference_key && Runtime::Identity.from(aggregate, args, reference_key))
422
+ return nil unless id
423
+
424
+ record = runtime.registry.repository(domain_name, aggregate).find(id)
425
+ return nil unless record
426
+
427
+ rules = Runtime::CommandRules.new(runtime.registry)
428
+ recomputed_kind = begin
429
+ rules.enforce_givens(record.dup, command, args, domain: domain_name, declaring: aggregate)
430
+
431
+ # A SECOND, SEPARATE DISPATCH_ORDER STEP — `enforce_givens`
432
+ # (just above) only ever checks a per-COMMAND `from:` clause
433
+ # (its own trailing `enforce_lifecycle_guard(declaring, ...)
434
+ # if declaring` call) — the aggregate's own `lifecycle do
435
+ # transition "X" => Y, from: Z end` block is a WHOLLY separate
436
+ # method (`admissible_transition`), called as its own later
437
+ # DISPATCH_ORDER step (`:enforce_givens` then
438
+ # `:admissible_transition` — Vocabulary.symbols
439
+ # ("AggregateDispatchOrder")), not reached from inside
440
+ # `enforce_givens` at all. Missing this call meant a command
441
+ # declared with NO per-command `from:` of its own — every real
442
+ # transition-guarded command in this corpus, `Admit`/`Reject`
443
+ # included — always recomputed "admitted" no matter the
444
+ # record's actual state, because the ONE check that would
445
+ # have refused it was never run. Found live: `Expression::
446
+ # Expression.Admit`, fuzzed against `lib/hecks/grammar`
447
+ # (a domain the property's own hand-verification — Banking,
448
+ # Pizzas — never happened to exercise a transition-guarded,
449
+ # no-per-command-`from:` command against). Called only when
450
+ # `enforce_givens` didn't already refuse, mirroring the real
451
+ # pipeline's own "first refusal wins" order exactly.
452
+ rules.admissible_transition(aggregate, command, record.dup)
453
+ nil
454
+ rescue *GUARD_REFUSAL_CLASSES => e
455
+ e.class.name
456
+ end
457
+
458
+ { verb: verb, domain: domain_name, aggregate: aggregate_name, command: command_name, id: id,
459
+ recomputed_refused: !recomputed_kind.nil?, recomputed_kind: recomputed_kind }
460
+ rescue StandardError
461
+ nil
462
+ end
463
+
464
+ # THE MUTATION ORACLE'S OWN PRE-DISPATCH READ — scoped, on
465
+ # purpose, to ENTITY-DISPATCHED commands only (a dotted
466
+ # command_name): the one place `append`/`remove`/`multiply`/
467
+ # `clamp` are known to act on an entity's OWN attributes
468
+ # (spec/fixtures/entity_list_mutations' own TaggedList — `tags`
469
+ # a value-object list, `count` a VO-typed scalar), never on
470
+ # ANOTHER nested entity list — so this never needs to reproduce
471
+ # `MutationApplier#entity_element`'s own auto-mint/collision logic
472
+ # (item 1's own fix) at all. An aggregate-level command whose OWN
473
+ # mutation appends an ENTITY (`Board.AddList`, `SafeDepositBox.
474
+ # LogVisit`) is a DIFFERENT, already-covered case — item 1's own
475
+ # collision property, not this one.
476
+ #
477
+ # `nil` for anything out of scope: an aggregate-level command, an
478
+ # entity command with no mutations at all, or one whose identity
479
+ # args (parent OR element) don't resolve.
480
+ def build_mutation_trace(runtime, verb, args)
481
+ domain_name, aggregate_name, command_name = Naming.split_verb(verb)
482
+ return nil unless command_name && command_name.include?(".")
483
+
484
+ aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
485
+ return nil unless aggregate
486
+
487
+ entity_name, entity_command_name = command_name.split(".", 2)
488
+ entity = aggregate.entities.find { |candidate| candidate.hecks_name == entity_name }
489
+ command = entity&.command(entity_command_name)
490
+ return nil unless command && command.mutations.any?
491
+
492
+ reference_key = command.references.to_s.empty? ? nil : Naming.reference_key(command.references)
493
+ parent_id = Runtime::Identity.of(aggregate, args) ||
494
+ Runtime::Identity.from(aggregate, args, :id) ||
495
+ (reference_key && Runtime::Identity.from(aggregate, args, reference_key))
496
+ return nil unless parent_id
497
+
498
+ record = runtime.registry.repository(domain_name, aggregate).find(parent_id)
499
+ return nil unless record
500
+
501
+ list_attr = aggregate.attributes.find { |a| a.list? && a.type.to_s == entity.hecks_name }
502
+ return nil unless list_attr
503
+
504
+ wants = entity.identity_paths.map do |path|
505
+ head = path.to_s.split(".").first.to_sym
506
+ raw = args[head]
507
+ return nil if raw.nil?
508
+
509
+ [head, Runtime::Value.for_attribute(aggregate, entity.attribute(head), raw)]
510
+ end
511
+
512
+ element = Array(record.state[list_attr.name]).find { |el| wants.all? { |head, want| el[head] == want } }
513
+ return nil unless element
514
+
515
+ { verb: verb, domain: domain_name, aggregate: aggregate_name, command: command_name,
516
+ parent_id: parent_id, list_attr: list_attr.name, element_wants: wants,
517
+ before: Runtime::Value.materialize(element), args: args }
518
+ rescue StandardError
519
+ nil
520
+ end
521
+
522
+ # THE SAME ELEMENT, RE-LOCATED, AFTER dispatch — by identity, not
523
+ # position (an append could have changed the array's own length
524
+ # or order relative to it). `nil` if it somehow vanished (not
525
+ # expected for any op this fixture declares — none of them
526
+ # remove the acted-on element itself — but a property comparing
527
+ # against `nil` fails loudly rather than crashing this replay).
528
+ def read_mutation_after(runtime, trace)
529
+ aggregate = runtime.registry.bluebook(trace[:domain])&.aggregate(trace[:aggregate])
530
+ return nil unless aggregate
531
+
532
+ record = runtime.registry.repository(trace[:domain], aggregate).find(trace[:parent_id])
533
+ return nil unless record
534
+
535
+ element = Array(record.state[trace[:list_attr]]).find do |el|
536
+ trace[:element_wants].all? { |head, want| el[head] == want }
537
+ end
538
+ element && Runtime::Value.materialize(element)
539
+ rescue StandardError
540
+ nil
541
+ end
542
+
543
+ # THE FAN-OUT ORACLE — one finding per (event, for_each policy) this
544
+ # step's own announced events could have triggered, independent of
545
+ # `PolicyInterpreter#deliver_for_each`: the SAME `where` evaluator
546
+ # every given/ensures already runs through, but the QUERY answered
547
+ # by `Ports::Query::InMemory.holds?` directly against the live
548
+ # repository (`Replay.run_filter`'s own idiom), never by calling
549
+ # `QueryInterpreter` — sharing that call would make this oracle
550
+ # blind to exactly the code the fan-out feature adds.
551
+ #
552
+ # `expected_row_ids` is `nil` when `where` did not hold — no dispatch
553
+ # is the claim, not "dispatched to zero rows," so a property
554
+ # comparing this against the reaction log needs to tell the two
555
+ # apart. Recomputed once per event, not once per policy-and-event,
556
+ # because a `Chapter` de-duplicates on nothing this loop cannot
557
+ # cheaply repeat.
558
+ def fan_out_findings(runtime, snapshot, announced, reactions_since)
559
+ announced.each_with_object([]) do |event, findings|
560
+ # `event.aggregate` is domain-qualified ("Banking::Account" —
561
+ # see command_rules/emission.rb's own Event.new) — the SAME
562
+ # source `PolicyInterpreter#policies_for` reads, split the
563
+ # same two ways: `Naming.demodulise` for the emitting
564
+ # aggregate's bare name, plain `split("::")` for the domain.
565
+ domain = event.aggregate.to_s.split("::").first
566
+ bluebook = runtime.registry.bluebook(domain)
567
+ next unless bluebook
568
+
569
+ emitting = Naming.demodulise(event.aggregate)
570
+
571
+ bluebook.policies.each do |policy|
572
+ next unless policy.fans_out? && policy.event_name == event.name
573
+ next unless policy.event_qualifier.nil? || policy.event_qualifier == emitting
574
+
575
+ findings << fan_out_finding(runtime, snapshot, policy, event, domain, reactions_since)
576
+ end
577
+ end
578
+ end
579
+
580
+ def fan_out_finding(runtime, snapshot, policy, event, domain, reactions_since)
581
+ payload = event.payload.transform_keys(&:to_sym)
582
+ held = policy.where.to_s.empty? ||
583
+ Bluebook::Expression::Evaluator.call(policy.where, {}, payload)
584
+
585
+ expected = held ? expected_fan_out_rows(runtime, snapshot, policy, domain, payload) : nil
586
+
587
+ actual = reactions_since.select { |r| r[:policy] == policy.name && r[:on] == event.name }
588
+ .filter_map { |r| r[:for_row] }
589
+
590
+ { policy: policy.name, on: event.name, expected_row_ids: expected, actual_row_ids: actual }
591
+ end
592
+
593
+ # THE INDEPENDENT RECOMPUTATION — `policy.for_each`'s declared query,
594
+ # answered against the PRE-DISPATCH snapshot (see the snapshot's
595
+ # own comment at its capture site: the real fan-out's query runs
596
+ # synchronously, before its own dispatched commands can mutate
597
+ # anything the query would have matched, so this has to read the
598
+ # same "before" state or it grades the wrong moment). Comparators
599
+ # are `Ports::Query::InMemory.holds?`, never `QueryInterpreter` —
600
+ # sharing that call would make this oracle blind to exactly the
601
+ # code the fan-out feature adds. A `Symbol` where-value binds to
602
+ # the triggering event's own payload (the same binding
603
+ # `OpenForCustomer`'s `customer_id: :customer_id` relies on); a
604
+ # literal is compared as declared.
605
+ def expected_fan_out_rows(runtime, snapshot, policy, domain, payload)
606
+ query_domain, aggregate_name, query_name = policy.for_each_route(domain)
607
+ aggregate = runtime.registry.bluebook(query_domain)&.aggregate(aggregate_name)
608
+ query = aggregate&.query(query_name)
609
+ return [] unless query
610
+
611
+ rows = snapshot[[query_domain, aggregate_name]] || {}
612
+ matched = rows.select do |_id, state|
613
+ query.wheres.all? do |clause|
614
+ held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, clause.field))
615
+ Ports::Query::InMemory.holds?(clause, held, payload)
616
+ end
617
+ end
618
+
619
+ matched.keys.map(&:to_s).sort
620
+ end
621
+
622
+ # Answers ONE ad hoc filter step for real — the mirror image of
623
+ # kernel/cli.rs's own `run_filter`, deliberately calling the exact
624
+ # SAME production module that method's Rust port stands in for
625
+ # (`Ports::Query::InMemory`, lib/hecks/ports/query/in_memory.rb)
626
+ # rather than re-deriving comparator behavior by hand. `field` walks
627
+ # through `QuerySpecification::FieldPath.dig` (the same reading a
628
+ # declared where-clause gets), `comparable`/`holds?` are the same
629
+ # two calls `InMemory.execute` itself makes per candidate record —
630
+ # this is that method's own filter/select step, inlined, because
631
+ # there is no DECLARED `Query` object here to hand `execute` (an ad
632
+ # hoc filter has no `order_by`/`limit`/`offset` at all, so nothing
633
+ # about `execute`'s own ordering/paging logic even applies).
634
+ # Sorted by id ascending regardless — `Ports::Query::Ordering`'s own
635
+ # header explains why an ask with no declared order still needs
636
+ # this tier ("the identity tier is what makes an ask total").
637
+ def run_filter(runtime, filter)
638
+ aggregate_ref = filter["aggregate"].to_s
639
+ field = filter["field"].to_s
640
+ op = filter["op"].to_s
641
+ value = filter["value"]
642
+
643
+ raise "unknown query comparator #{op.inspect}" unless FILTER_COMPARATORS.include?(op)
644
+
645
+ domain_name, aggregate_name = aggregate_ref.split("::", 2)
646
+ aggregate = runtime.registry.bluebook(domain_name)&.aggregate(aggregate_name)
647
+ raise "unknown aggregate #{aggregate_ref.inspect}" unless aggregate
648
+
649
+ clause = QuerySpecification::Common::WhereClause.new(field: field, op: op, value: value)
650
+ records = runtime.registry.repository(domain_name, aggregate).all
651
+ matched = records.select do |record|
652
+ held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(record, field))
653
+ Ports::Query::InMemory.holds?(clause, held, {})
654
+ end
655
+
656
+ matched.sort_by { |record| record.id.to_s }.map { |record| { id: record.id }.merge(record.state) }
657
+ end
658
+
659
+ # The `refusals` entry's own "verb" column for a REFUSED ad hoc
660
+ # filter — there is no real verb to report (a filter step carries
661
+ # none), so this builds the SAME descriptive label kernel/cli.rs's
662
+ # own `filter_label` builds from the same three raw fields, tolerant
663
+ # of any of them being missing (Ruby's own nil-to-"" interpolation)
664
+ # the same way that Rust port is.
665
+ def filter_label(filter) = "filter #{filter["aggregate"]}.#{filter["field"]} #{filter["op"]}"
666
+ end
667
+ end
668
+ end