hecks 2.7.0 → 2.9.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 (463) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +11 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +18 -99
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres.rb +145 -134
  25. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  26. data/lib/hecks/adapters/driven/prism.rb +7 -30
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  31. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  32. data/lib/hecks/adapters/driven/tenant_provisioner.rb +15 -37
  33. data/lib/hecks/adapters/driven.rb +5 -36
  34. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  35. data/lib/hecks/behaviors/dsl.rb +7 -25
  36. data/lib/hecks/behaviors/expectations.rb +32 -142
  37. data/lib/hecks/behaviors/ir.rb +7 -20
  38. data/lib/hecks/behaviors/rspec.rb +6 -17
  39. data/lib/hecks/behaviors/runner.rb +10 -34
  40. data/lib/hecks/behaviors.rb +4 -8
  41. data/lib/hecks/bluebook/aggregate.rb +9 -75
  42. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  43. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  44. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  45. data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
  46. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  47. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  48. data/lib/hecks/bluebook/assembly.rb +6 -45
  49. data/lib/hecks/bluebook/attribute.rb +12 -61
  50. data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
  51. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  52. data/lib/hecks/bluebook/behaviour/chapter.rb +17 -42
  53. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  54. data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
  55. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  56. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  57. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  58. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  59. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  60. data/lib/hecks/bluebook/behaviour/read_model.rb +38 -26
  61. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  62. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  63. data/lib/hecks/bluebook/capabilities.rb +17 -49
  64. data/lib/hecks/bluebook/chapter.rb +10 -48
  65. data/lib/hecks/bluebook/command.rb +23 -81
  66. data/lib/hecks/bluebook/domain_port.rb +14 -63
  67. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  68. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  69. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  70. data/lib/hecks/bluebook/dsl/binding_proxy.rb +17 -65
  71. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
  72. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
  73. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  74. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  75. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  76. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
  77. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  78. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  79. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -189
  80. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  81. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  82. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  83. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  84. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  85. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  86. data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
  87. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  88. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  89. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  90. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  91. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  92. data/lib/hecks/bluebook/dsl/world_builder.rb +19 -106
  93. data/lib/hecks/bluebook/entity.rb +13 -71
  94. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  95. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  96. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  97. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  98. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  99. data/lib/hecks/bluebook/expression/resolver.rb +37 -660
  100. data/lib/hecks/bluebook/hexagon.rb +5 -28
  101. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  102. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  103. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  104. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  105. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  106. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  107. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
  108. data/lib/hecks/bluebook/meta_validator/shapes.rb +54 -268
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +47 -308
  110. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  111. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  112. data/lib/hecks/bluebook/meta_validator.rb +79 -349
  113. data/lib/hecks/bluebook/model_check/client_profile.rb +10 -91
  114. data/lib/hecks/bluebook/model_check.rb +102 -600
  115. data/lib/hecks/bluebook/pattern_subset.rb +11 -69
  116. data/lib/hecks/bluebook/process_manager.rb +13 -83
  117. data/lib/hecks/bluebook/project_loader.rb +3 -39
  118. data/lib/hecks/bluebook/project_register.rb +12 -60
  119. data/lib/hecks/bluebook/query.rb +9 -48
  120. data/lib/hecks/bluebook/read_model.rb +57 -81
  121. data/lib/hecks/bluebook/reference.rb +7 -41
  122. data/lib/hecks/bluebook/smoke_test.rb +8 -98
  123. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  124. data/lib/hecks/bluebook/translation.rb +26 -62
  125. data/lib/hecks/bluebook/value_object.rb +11 -36
  126. data/lib/hecks/bluebook.rb +5 -22
  127. data/lib/hecks/cache_dir.rb +84 -0
  128. data/lib/hecks/cli/document.rb +86 -0
  129. data/lib/hecks/cli/ir.rb +45 -0
  130. data/lib/hecks/cli/mcp.rb +31 -0
  131. data/lib/hecks/cli/mcp_door.rb +366 -0
  132. data/lib/hecks/cli/model_check.rb +203 -0
  133. data/lib/hecks/cli/project_cli.rb +90 -0
  134. data/lib/hecks/cli/project_diagrams.rb +36 -0
  135. data/lib/hecks/cli/run.rb +173 -0
  136. data/lib/hecks/cli/smoke_test.rb +45 -0
  137. data/lib/hecks/cli/stores.rb +53 -0
  138. data/lib/hecks/cli.rb +160 -0
  139. data/lib/hecks/construct.rb +8 -34
  140. data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
  141. data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
  142. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  143. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  144. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  145. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  146. data/lib/hecks/embryonaut_bluebook.rb +4 -71
  147. data/lib/hecks/facade/cli_door.rb +24 -106
  148. data/lib/hecks/facade/cli_runner.rb +28 -172
  149. data/lib/hecks/facade/command_request.rb +13 -33
  150. data/lib/hecks/facade/handle.rb +34 -121
  151. data/lib/hecks/facade/json_door.rb +31 -146
  152. data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
  153. data/lib/hecks/facade/surface/chapter.rb +10 -54
  154. data/lib/hecks/facade/surface.rb +8 -23
  155. data/lib/hecks/facade.rb +10 -25
  156. data/lib/hecks/forms/app.rb +15 -70
  157. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  158. data/lib/hecks/forms/field_renderer.rb +17 -156
  159. data/lib/hecks/forms/field_shape.rb +30 -222
  160. data/lib/hecks/forms/html.rb +12 -47
  161. data/lib/hecks/forms/index_renderer.rb +1 -4
  162. data/lib/hecks/forms/page.rb +3 -9
  163. data/lib/hecks/forms/params.rb +14 -60
  164. data/lib/hecks/forms/port_argument.rb +2 -19
  165. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  166. data/lib/hecks/forms/record_renderer.rb +8 -75
  167. data/lib/hecks/forms/record_table.rb +7 -50
  168. data/lib/hecks/forms/reference_options.rb +3 -30
  169. data/lib/hecks/forms/value_object_shape.rb +8 -35
  170. data/lib/hecks/forms.rb +5 -33
  171. data/lib/hecks/fqn.rb +5 -12
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  175. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  176. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  177. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  178. data/lib/hecks/framework.rb +11 -80
  179. data/lib/hecks/freezer.rb +5 -39
  180. data/lib/hecks/grammar/expression.bluebook +15 -40
  181. data/lib/hecks/grammar/translation.bluebook +3 -6
  182. data/lib/hecks/grammar.rb +16 -41
  183. data/lib/hecks/ir.rb +14 -83
  184. data/lib/hecks/language/adapter.bluebook +2 -7
  185. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  186. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  187. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
  188. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  189. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  190. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  191. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  192. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  193. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  194. data/lib/hecks/language/bluebook/query.bluebook +12 -43
  195. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  196. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  197. data/lib/hecks/language/bluebook/vocabulary.bluebook +98 -197
  198. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  199. data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
  200. data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
  201. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  202. data/lib/hecks/language/oidc.json +35 -0
  203. data/lib/hecks/language/port.bluebook +4 -17
  204. data/lib/hecks/language/translation/translation.bluebook +3 -9
  205. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  206. data/lib/hecks/language/world/wiring.bluebook +1 -3
  207. data/lib/hecks/language/world/world.bluebook +5 -7
  208. data/lib/hecks/literal.rb +8 -50
  209. data/lib/hecks/mcp_door_scope.rb +95 -0
  210. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  211. data/lib/hecks/naming.rb +29 -137
  212. data/lib/hecks/ports/access_control.rb +7 -23
  213. data/lib/hecks/ports/agent/answers.rb +5 -23
  214. data/lib/hecks/ports/agent.rb +47 -172
  215. data/lib/hecks/ports/authentication.rb +12 -39
  216. data/lib/hecks/ports/authorization.rb +16 -43
  217. data/lib/hecks/ports/clock.rb +5 -45
  218. data/lib/hecks/ports/extraction.rb +7 -12
  219. data/lib/hecks/ports/identity_assignment.rb +8 -29
  220. data/lib/hecks/ports/identity_generation.rb +6 -29
  221. data/lib/hecks/ports/identity_resolution.rb +2 -10
  222. data/lib/hecks/ports/key_vault.rb +9 -45
  223. data/lib/hecks/ports/persistence/append_only.rb +62 -65
  224. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  225. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  226. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  227. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  228. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
  229. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
  231. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +7 -26
  232. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
  233. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
  250. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  251. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  252. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  253. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  254. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  255. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  256. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
  257. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +22 -122
  258. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  259. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  260. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  261. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  262. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  263. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  264. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  266. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  267. data/lib/hecks/ports/persistence/state_codec.rb +23 -103
  268. data/lib/hecks/ports/persistence.rb +7 -14
  269. data/lib/hecks/ports/projection.rb +42 -84
  270. data/lib/hecks/ports/query/in_memory.rb +10 -58
  271. data/lib/hecks/ports/query/ordering.rb +11 -32
  272. data/lib/hecks/ports/query.rb +14 -37
  273. data/lib/hecks/ports.rb +2 -8
  274. data/lib/hecks/projections/bootstrap_table.rb +11 -43
  275. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  276. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  277. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  278. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  279. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  280. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  281. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  282. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  283. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  284. data/lib/hecks/projections/deploy/fargate.rb +35 -202
  285. data/lib/hecks/projections/deploy/lambda.rb +151 -679
  286. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  287. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  288. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  289. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  290. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  291. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  292. data/lib/hecks/projections/deploy/preview/template.rb +2 -58
  293. data/lib/hecks/projections/deploy/preview.rb +2 -67
  294. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  295. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  296. data/lib/hecks/projections/deploy/shared.rb +47 -146
  297. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  298. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  299. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  300. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  301. data/lib/hecks/projections/diagrams.rb +20 -582
  302. data/lib/hecks/projections/glossary/html.rb +14 -130
  303. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  304. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  305. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  306. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  307. data/lib/hecks/projections/glossary.rb +30 -222
  308. data/lib/hecks/projections/ir.rb +3 -10
  309. data/lib/hecks/projections/model/deviations.rb +15 -42
  310. data/lib/hecks/projections/model.rb +12 -51
  311. data/lib/hecks/projections/oidc.rb +12 -56
  312. data/lib/hecks/projections/parser_table.rb +9 -28
  313. data/lib/hecks/projections/reference.rb +9 -16
  314. data/lib/hecks/projections/rust_vocabulary.rb +12 -250
  315. data/lib/hecks/projections/shape.rb +3 -29
  316. data/lib/hecks/projections/statements.rb +7 -86
  317. data/lib/hecks/projections/vocabulary.rb +6 -22
  318. data/lib/hecks/projections.rb +2 -16
  319. data/lib/hecks/projector/cli_projector.rb +60 -375
  320. data/lib/hecks/projector/docs_projector.rb +10 -201
  321. data/lib/hecks/projector/exporter.rb +46 -138
  322. data/lib/hecks/projector/ir_projector.rb +2 -9
  323. data/lib/hecks/projector/narrate_projector.rb +15 -189
  324. data/lib/hecks/projector/target.rb +13 -76
  325. data/lib/hecks/projector.rb +30 -121
  326. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  327. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  328. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  329. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  330. data/lib/hecks/query_specification/common/options.rb +2 -6
  331. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  332. data/lib/hecks/query_specification/field_path.rb +37 -77
  333. data/lib/hecks/query_specification/hop_path.rb +12 -76
  334. data/lib/hecks/release/gem_pin.rb +2 -26
  335. data/lib/hecks/release/runner/ci_publisher.rb +93 -0
  336. data/lib/hecks/release/runner/commands.rb +66 -0
  337. data/lib/hecks/release/runner/console.rb +52 -0
  338. data/lib/hecks/release/runner/gem_publisher.rb +43 -0
  339. data/lib/hecks/release/runner/git.rb +48 -0
  340. data/lib/hecks/release/runner/npm_publisher.rb +75 -0
  341. data/lib/hecks/release/runner/preflight.rb +104 -0
  342. data/lib/hecks/release/runner/published.rb +55 -0
  343. data/lib/hecks/release/runner/tagger.rb +80 -0
  344. data/lib/hecks/release/runner.rb +215 -0
  345. data/lib/hecks/rendering.rb +4 -26
  346. data/lib/hecks/router/namespace_installer.rb +12 -36
  347. data/lib/hecks/router.rb +4 -8
  348. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  349. data/lib/hecks/runtime/boot_gates.rb +3 -18
  350. data/lib/hecks/runtime/caller.rb +19 -59
  351. data/lib/hecks/runtime/capability_graph.rb +5 -21
  352. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  353. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  354. data/lib/hecks/runtime/command_interpreter.rb +125 -307
  355. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  356. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  357. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  358. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  359. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  360. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  361. data/lib/hecks/runtime/dispatcher.rb +82 -241
  362. data/lib/hecks/runtime/entity_element.rb +40 -462
  363. data/lib/hecks/runtime/entity_interpreter.rb +65 -220
  364. data/lib/hecks/runtime/errors.rb +32 -115
  365. data/lib/hecks/runtime/event.rb +3 -25
  366. data/lib/hecks/runtime/identity.rb +18 -154
  367. data/lib/hecks/runtime/instance.rb +18 -150
  368. data/lib/hecks/runtime/interpreting.rb +11 -70
  369. data/lib/hecks/runtime/invocation.rb +28 -164
  370. data/lib/hecks/runtime/loader.rb +48 -186
  371. data/lib/hecks/runtime/outbox.rb +38 -261
  372. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  373. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  374. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  375. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  376. data/lib/hecks/runtime/read_model_interpreter.rb +139 -208
  377. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  378. data/lib/hecks/runtime/reference_hop.rb +8 -81
  379. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  380. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  381. data/lib/hecks/runtime/registry/verification.rb +52 -225
  382. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  383. data/lib/hecks/runtime/registry.rb +103 -244
  384. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  385. data/lib/hecks/runtime/routing.rb +4 -15
  386. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  387. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  388. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  389. data/lib/hecks/runtime/tenant_check.rb +6 -68
  390. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  391. data/lib/hecks/runtime/value/admission.rb +16 -67
  392. data/lib/hecks/runtime/value/coercion.rb +155 -641
  393. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  394. data/lib/hecks/runtime/value.rb +12 -84
  395. data/lib/hecks/runtime.rb +17 -65
  396. data/lib/hecks/storehouse.rb +146 -567
  397. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  398. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  399. data/lib/hecks/vendoring/git_environment.rb +2 -8
  400. data/lib/hecks/vendoring/git_source.rb +4 -10
  401. data/lib/hecks/vendoring.rb +7 -46
  402. data/lib/hecks/version.rb +3 -14
  403. data/lib/hecks/vocabulary.rb +5 -0
  404. data/lib/hecks.rb +32 -82
  405. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  406. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  407. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  408. metadata +30 -59
  409. data/lib/hecks/bench/cli.rb +0 -74
  410. data/lib/hecks/bench/environment.rb +0 -100
  411. data/lib/hecks/bench/postgres_probe.rb +0 -41
  412. data/lib/hecks/bench/report.rb +0 -116
  413. data/lib/hecks/bench/ruby_runner.rb +0 -151
  414. data/lib/hecks/bench/run.rb +0 -55
  415. data/lib/hecks/bench/rust_runner.rb +0 -165
  416. data/lib/hecks/bench/stats.rb +0 -75
  417. data/lib/hecks/bench/suite.rb +0 -172
  418. data/lib/hecks/bench/workload.rb +0 -137
  419. data/lib/hecks/bench.rb +0 -28
  420. data/lib/hecks/codemod.rb +0 -443
  421. data/lib/hecks/corpus.rb +0 -479
  422. data/lib/hecks/doc/reference.rb +0 -585
  423. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  424. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  425. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  426. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  427. data/lib/hecks/fuzzing/differential.rb +0 -192
  428. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  429. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  430. data/lib/hecks/fuzzing/form_census.rb +0 -285
  431. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  432. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  433. data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
  434. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  435. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  436. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  437. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  438. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  439. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  440. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  441. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  442. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  443. data/lib/hecks/fuzzing/properties.rb +0 -267
  444. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  445. data/lib/hecks/fuzzing/replay.rb +0 -930
  446. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  447. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  448. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  449. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  450. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  451. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  452. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  453. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  454. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  455. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  456. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  457. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  458. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  459. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  460. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  461. data/lib/hecks/fuzzing.rb +0 -23
  462. data/lib/hecks/grammar/evolve.rb +0 -524
  463. data/lib/hecks/query_ir.rb +0 -482
@@ -1,399 +0,0 @@
1
- module Hecks
2
- module Fuzzing
3
- module Properties
4
- # Stored-record, saga-rehydration, fan-out, and read-model-aggregation
5
- # properties: every stored record still satisfies its own declared
6
- # invariants, a saga rehydrates cleanly from a checkpoint, a policy's
7
- # own for_each/where fans out exactly once per matching row, and a
8
- # read model's count/median/group_by answers match an independent
9
- # recomputation.
10
-
11
- # Holds stored_records_satisfy_declared_invariants, sagas_rehydrate_cleanly,
12
- # fanout_dispatches_once_per_matching_row, aggregation_matches_recompute,
13
- # and group_by_matches_recompute, plus the recomputation helpers
14
- # (#check_piece_invariants, #eligible_rows, #nest_rows,
15
- # #recompute_median) each leans on.
16
- module InvariantsAndAggregation
17
- # Every stored record still satisfies its own aggregate's declared
18
- # invariants — Admissibility#enforce_invariants (command_rules/
19
- # admissibility.rb) checks these after every command's mutations,
20
- # before save, the same point `ensures` is checked. Nothing until
21
- # now re-checked a record after a whole replay finished, independent
22
- # of whichever call site was supposed to have refused a violation
23
- # in the first place — a record failing its own declared invariant
24
- # here is proof a violating write landed anyway: the call site
25
- # stopped calling enforce_invariants, or some other path (a
26
- # translation, a backfill) wrote around it entirely.
27
- #
28
- # `history[:instances]` entries are already plain, symbol-keyed
29
- # state Hashes (Replay.call's own `record.state`) — called against
30
- # Evaluator.call the same way ValueObject::Builder#build already
31
- # does for a VO's own invariants (value/coercion.rb), no GuardState
32
- # wrapper needed the way enforce_invariants' own live call uses one
33
- # (GuardState exists for `parent.`/projected-field dereferencing
34
- # mid-dispatch; a stored record's own scalar fields need none of
35
- # that to re-check a same-aggregate invariant against itself).
36
- #
37
- # Real target: Account's own `invariant("the balance never goes
38
- # negative") { balance.cents >= 0 }`.
39
- #
40
- # Entity#invariants (round 7) closes here too, not for free —
41
- # `stored_records_satisfy_declared_invariants` only ever checked
42
- # the aggregate's own flat state; a piece's own invariant is
43
- # checked against every element of a `list_of` field, a genuinely
44
- # different walk `check_piece_invariants` below makes,
45
- # independently of `Admissibility#check_entity_invariants` (the
46
- # live enforcement path this property exists to catch drifting
47
- # from) — same reasoning `stored_records_satisfy_declared_
48
- # invariants`' own top-level check already applies one level up.
49
- #
50
- # Real target: SafeDepositBox's own Visit — `invariant("a written
51
- # note is not blank") { !note || !note.text.to_s.empty? }`.
52
- #
53
- # @param history [Hash] a replayed history as returned by `Replay.call`
54
- # @return [true, String] true if every stored record satisfies its own
55
- # (and its own entities') declared invariants; otherwise a message
56
- # naming the record and the invariant it violates
57
- def stored_records_satisfy_declared_invariants(history)
58
- bluebooks = history.fetch(:bluebooks)
59
-
60
- offenders = history.fetch(:instances).filter_map do |key, state|
61
- domain_name = key.split("::").first
62
- aggregate_name = key.split("::").last.split("#").first
63
- bluebook = bluebooks[domain_name]
64
- aggregate = bluebook&.aggregate(aggregate_name)
65
- next unless aggregate
66
-
67
- violated = aggregate.invariants.find do |invariant|
68
- !Bluebook::Expression::Evaluator.call(invariant.canonical, state)
69
- end
70
- next "#{key} violates #{aggregate_name}'s own declared invariant #{violated.description.inspect}" if violated
71
-
72
- check_piece_invariants(aggregate, state, key)
73
- end
74
-
75
- offenders.empty? || offenders.join("; ")
76
- end
77
-
78
- # A piece's own invariant, checked against every element a
79
- # `list_of` field holds — the same lookup `Admissibility#
80
- # check_entity_invariants` makes (`owner.attributes.find { |a|
81
- # a.list? && a.type.to_s == entity.hecks_name }`), independently
82
- # reapplied here against a stored record's own plain Hash state
83
- # rather than a live `Instance`.
84
- # @param owner_construct [Bluebook::Aggregate, Bluebook::Entity] the
85
- # construct whose own `list_of` entities to check
86
- # @param owner_state [Hash] `owner_construct`'s own stored state
87
- # @param key [String] the top-level record key, for the message
88
- # @return [String, nil] a message naming the first violating piece found,
89
- # at any nesting depth; `nil` if every piece satisfies its invariants
90
- def check_piece_invariants(owner_construct, owner_state, key)
91
- owner_construct.entities.each do |entity|
92
- next if entity.invariants.empty?
93
-
94
- list_attr = owner_construct.attributes.find { |a| a.list? && a.type.to_s == entity.hecks_name }
95
- next unless list_attr
96
-
97
- Array(owner_state[list_attr.name]).each do |element|
98
- violated = entity.invariants.find do |invariant|
99
- !Bluebook::Expression::Evaluator.call(invariant.canonical, element)
100
- end
101
- if violated
102
- return "#{key}'s own #{entity.hecks_name} violates its declared invariant " \
103
- "#{violated.description.inspect}"
104
- end
105
-
106
- nested = check_piece_invariants(entity, element, key)
107
- return nested if nested
108
- end
109
- end
110
- nil
111
- end
112
-
113
- # A saga instance's own checkpoint survives being written and read
114
- # back — the durability contract `SagaInterpreter#checkpoint` makes
115
- # (`state:` plus a `deep_copy`d `memory:`, handed to whatever
116
- # adapter answers `save_saga`) and `Registry#rehydrate_sagas!`
117
- # promises to restore on the next boot (`each_saga` yielding
118
- # `[pm, correlation, state, memory]` back into `saga_instances`).
119
- # `Replay` captures the live store already materialised the same
120
- # way `checkpoint` itself does (`Value.materialize`, not raw
121
- # `Runtime::Value`s — see its own comment); this property pushes
122
- # that captured memory through the same `JSON.generate` then
123
- # `JSON.parse(symbolize_names: true)` round-trip `checkpoint`'s own
124
- # `deep_copy` performs (mirrored here rather than called — a
125
- # private instance method with no registry to hand it) and checks
126
- # it comes back byte-identical. A memory holding anything that
127
- # round-trip cannot carry faithfully — a bare Symbol leaf, a
128
- # non-JSON type a future field introduces — is corruption the
129
- # durable path would introduce on a real restart, caught here
130
- # without needing one.
131
- #
132
- # `declares_state?` (Behaviour::ProcessManager) is the other half:
133
- # a live or rehydrated instance sitting in a state the procedure
134
- # never declares is the saga-durability twin of
135
- # `lifecycle_values_are_declared` above.
136
- # @param history [Hash] a replayed history as returned by `Replay.call`
137
- # @return [true, String] true if every saga instance holds a declared
138
- # state and its own memory survives a checkpoint round-trip; otherwise a
139
- # message naming the process manager, correlation, and problem
140
- def sagas_rehydrate_cleanly(history)
141
- bluebook = history.fetch(:bluebook)
142
- process_managers = bluebook.process_managers.to_h { |pm| [pm.name, pm] }
143
-
144
- offenders = history.fetch(:saga_instances).flat_map do |pm_name, conversations|
145
- pm = process_managers[pm_name]
146
-
147
- conversations.filter_map do |correlation, instance|
148
- problems = []
149
-
150
- problems << "holds state #{instance[:state].inspect}, which #{pm_name} never declares" \
151
- if pm && !pm.declares_state?(instance[:state])
152
-
153
- rehydrated = JSON.parse(JSON.generate(instance[:memory]), symbolize_names: true)
154
- if rehydrated != instance[:memory]
155
- problems << "memory does not survive its own checkpoint round-trip " \
156
- "(checkpointed #{instance[:memory].inspect}, rehydrated #{rehydrated.inspect})"
157
- end
158
-
159
- next if problems.empty?
160
-
161
- "#{pm_name}##{correlation.inspect}: #{problems.join(' and ')}"
162
- end
163
- end
164
-
165
- offenders.empty? || offenders.join("; ")
166
- end
167
-
168
- # A `for_each` policy dispatches exactly once per row its declared
169
- # query answers — never once for the triggering event regardless of
170
- # row count, never skipping a matched row, never firing on a row a
171
- # concurrent mutation only made match after the fact. `Replay`
172
- # computes the expected row-id set independently, at the same
173
- # instant the real dispatch runs (`Replay.expected_fan_out_rows`,
174
- # the query oracle's own shape aimed at fan-out: two engines
175
- # compared, never one graded against itself), and records it
176
- # beside what the reaction log actually shows. `expected_row_ids`
177
- # is `nil`, not `[]`, when `policy.where` did not hold — no
178
- # dispatch is the claim then, not "dispatched to zero rows," and a
179
- # policy that dispatched anyway despite a failing guard is as real
180
- # a finding as a row it skipped.
181
- # @param history [Hash] a replayed history as returned by `Replay.call`
182
- # @return [true, String] true if every fan-out finding's actual dispatches
183
- # match its independently computed expected row set; otherwise a message
184
- # naming the policy, event, and disagreement
185
- def fanout_dispatches_once_per_matching_row(history)
186
- offenders = history.fetch(:fan_outs).filter_map do |finding|
187
- expected = finding[:expected_row_ids]
188
- actual = finding[:actual_row_ids].sort
189
-
190
- if expected.nil?
191
- next if actual.empty?
192
-
193
- next "#{finding[:policy]} on #{finding[:on]}: where did not hold, but dispatched to #{actual.inspect}"
194
- end
195
-
196
- next if actual == expected
197
-
198
- "#{finding[:policy]} on #{finding[:on]}: for_each answered #{expected.inspect}, " \
199
- "but the reaction log shows dispatches to #{actual.inspect}"
200
- end
201
-
202
- offenders.empty? || offenders.join("; ")
203
- end
204
-
205
- # A `count`/`median` report's reduced scalar matches the same
206
- # reduction done independently, over the same eligible rows —
207
- # `ReadModelInterpreter#project`'s own FK-join (root first, then
208
- # each many-side head matched against it) and `#median` (odd →
209
- # the true middle, even → the average of the two middles as a
210
- # Float, empty → `nil`; `count` is the filtered length, empty →
211
- # `0`), reproduced here in plain Ruby against `history[:instances]`
212
- # rather than a live registry — `FieldPath.dig` +
213
- # `Ports::Query::InMemory.comparable`/`.holds?` are the same two
214
- # calls the interpreter itself makes to read a field and judge a
215
- # `where`, called here rather than re-derived, so this oracle
216
- # cannot drift from what "read a field" or "a clause holds" mean
217
- # without the interpreter drifting the identical way.
218
- #
219
- # Only a report whose `:query` is answered by the same bluebook
220
- # `history[:bluebook]` carries (the bare `Domain.report_name`
221
- # form, `domain == bluebook.name`) is checked — the same "only
222
- # what we have the grammar for" scope `lifecycle_values_are_declared`
223
- # already takes for a multi-domain replay.
224
- # See the comment above: a closed chain of eligibility guards
225
- # ("only a report answered by this bluebook, only a count/median
226
- # report, only one with a reduced many-side head") each gating the
227
- # next, ending in one independent recomputation compared against
228
- # the live answer. The guards are what make this oracle scoped
229
- # correctly, not incidental complexity — narrower than "every
230
- # branch reads as its own precondition."
231
- # rubocop:disable-next Metrics/CyclomaticComplexity
232
- # rubocop:disable-next Metrics/PerceivedComplexity
233
- # @param history [Hash] a replayed history as returned by `Replay.call`
234
- # @return [true, String] true if every eligible count/median report answer
235
- # matches an independent recomputation; otherwise a message naming the
236
- # query and the disagreement
237
- def aggregation_matches_recompute(history)
238
- bluebook = history.fetch(:bluebook)
239
-
240
- offenders = history.fetch(:queries).filter_map do |asked|
241
- next if asked[:error]
242
-
243
- domain, name = asked[:query].to_s.split(".", 2)
244
- next unless name && domain == bluebook.name
245
-
246
- model = bluebook.read_model(name)
247
- next unless model && (model.count? || model.median_field)
248
-
249
- reduced_head = model.aggregate_heads.find { |head| head[:many] }
250
- next unless reduced_head
251
-
252
- rows = eligible_rows(bluebook, asked.fetch(:instances_at), domain, model, reduced_head, asked[:args] || {})
253
- expected = model.count? ? rows.length : recompute_median(rows, model.median_field)
254
- actual = asked[:rows]&.first&.dig(reduced_head[:as])
255
- next if actual == expected
256
-
257
- "#{asked[:query]} #{asked[:args].inspect} answered #{actual.inspect} for #{reduced_head[:as]}, " \
258
- "but recomputing independently from #{rows.length} eligible row(s) gives #{expected.inspect}"
259
- end
260
-
261
- offenders.empty? || offenders.join("; ")
262
- end
263
-
264
- # `aggregation_matches_recompute`'s own shape, extended from
265
- # reducing a many-side head to a scalar (count/median) to nesting
266
- # it — `ReadModelInterpreter#group_by_target`/`#nest`, reproduced
267
- # here in plain Ruby against `history[:instances]` the same way
268
- # `eligible_rows` already reproduces the FK-join and `where`
269
- # narrowing count/median share. `Value.materialize_unwrapped` is
270
- # the same call `#project` makes before nesting (a single-field
271
- # value object recurses to its bare scalar — a real grouping key
272
- # has to be one) — called here rather than re-derived, so this
273
- # oracle cannot drift from what "the group key" means without the
274
- # interpreter drifting the identical way.
275
- #
276
- # Real target: AccountsByKind (`group_by :kind, :number`,
277
- # rootless — always generator-eligible with `{}` args).
278
- # Same closed eligibility-guard chain as aggregation_matches_
279
- # recompute just above (see its own comment) — group_by in place
280
- # of count/median, nest_rows in place of recompute_median.
281
- # rubocop:disable-next Metrics/CyclomaticComplexity
282
- # rubocop:disable-next Metrics/PerceivedComplexity
283
- #
284
- # @param history [Hash] a replayed history as returned by `Replay.call`
285
- # @return [true, String] true if every eligible group_by report answer matches
286
- # an independent recompute; otherwise a semicolon-joined message naming each
287
- # offending query
288
- def group_by_matches_recompute(history)
289
- bluebook = history.fetch(:bluebook)
290
-
291
- offenders = history.fetch(:queries).filter_map do |asked|
292
- next if asked[:error]
293
-
294
- domain, name = asked[:query].to_s.split(".", 2)
295
- next unless name && domain == bluebook.name
296
-
297
- model = bluebook.read_model(name)
298
- next unless model&.group_by&.any?
299
-
300
- grouped_head = model.aggregate_heads.find { |head| head[:many] }
301
- next unless grouped_head
302
-
303
- rows = eligible_rows(bluebook, asked.fetch(:instances_at), domain, model, grouped_head, asked[:args] || {})
304
- materialized = rows.map { |state| Runtime::Value.materialize_unwrapped(state) }
305
- expected = nest_rows(materialized, model.group_by_fields)
306
- actual = asked[:rows]&.first&.dig(grouped_head[:as])
307
- next if actual == expected
308
-
309
- "#{asked[:query]} #{asked[:args].inspect} answered a #{grouped_head[:as]} grouping that disagrees " \
310
- "with independently nesting group_by #{model.group_by_fields.inspect} over #{rows.length} " \
311
- "eligible row(s)"
312
- end
313
-
314
- offenders.empty? || offenders.join("; ")
315
- end
316
-
317
- # `ReadModelInterpreter#nest`, byte for byte: one level of nesting
318
- # per `group_by` field in declared order, leaf is the row with
319
- # every grouped field stripped (already spent, as the keys that
320
- # reached it).
321
- # @param rows [Array<Hash>] materialized, symbol-keyed rows to nest
322
- # @param fields [Array<Symbol>] the `group_by` fields, in declared order
323
- # @return [Hash] one level of nesting per field, in order; the leaf under
324
- # each key path is the first row in that group with every grouped field
325
- # stripped
326
- def nest_rows(rows, fields)
327
- field, *rest = fields
328
- rows.group_by { |row| row[field] }.transform_values do |group|
329
- stripped = group.map { |row| row.reject { |key, _| key == field } }
330
- rest.empty? ? stripped.first : nest_rows(stripped, rest)
331
- end
332
- end
333
-
334
- # The eligible rows a `count`/`median` head reduces — every
335
- # instance of the reduced head's own aggregate, FK-matched against
336
- # the report's root reference (if it has one; a rootless report has
337
- # none to match) exactly the way `ReadModelInterpreter#reference_fields`
338
- # finds the matching attribute, then narrowed by the report's own
339
- # `where` clauses via the same `InMemory.holds?` the interpreter's
340
- # `execute` calls.
341
- # @param bluebook [Bluebook::Chapter] the bluebook the report belongs to
342
- # @param instances [Hash] the snapshot to read rows from (`history[:instances]`
343
- # shape, or the query's own `instances_at`)
344
- # @param domain [String] the domain name the reduced aggregate belongs to
345
- # @param model [Bluebook::ReadModel] the report being recomputed
346
- # @param reduced_head [Hash] the many-side aggregate head being reduced, from
347
- # `model.aggregate_heads`
348
- # @param args [Hash] the query's own arguments, for FK matching and `where`
349
- # evaluation
350
- # @return [Array<Hash>] every eligible row's own state, `id:` merged in
351
- def eligible_rows(bluebook, instances, domain, model, reduced_head, args)
352
- aggregate = bluebook.aggregate(reduced_head[:aggregate])
353
- prefix = "#{domain}::#{reduced_head[:aggregate]}#"
354
- # `id:` merged in, the same `record.to_h` (`@state.merge(id:
355
- # @id)`) every live head row carries — count/median never read
356
- # it, but group_by_matches_recompute's own independent nesting
357
- # does, the same way ReadModelInterpreter#row(record) = record.
358
- # to_h does for the live path it's checking against.
359
- rows = instances.filter_map { |key, state| state.merge(id: key.split("#").last) if key.start_with?(prefix) }
360
-
361
- if model.reference_target
362
- reference_id = args[model.reference_name].to_s
363
- fk_fields = aggregate.attributes.select do |attribute|
364
- attribute.reference? && attribute.type.target_name == model.reference_target.to_s
365
- end.map(&:name)
366
-
367
- rows = rows.select { |state| fk_fields.any? { |field| state[field].to_s == reference_id } }
368
- end
369
-
370
- rows.select do |state|
371
- model.wheres.all? do |clause|
372
- held = Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, clause.field))
373
- Ports::Query::InMemory.holds?(clause, held, args)
374
- end
375
- end
376
- end
377
-
378
- # `ReadModelInterpreter#median`'s own definition, reproduced byte
379
- # for byte: odd count → the true middle value, sorted; even count
380
- # → the average of the two middle values, as a Float; empty → nil,
381
- # never zero, so a caller cannot mistake "nothing to average" for
382
- # "averaged to zero."
383
- # @param rows [Array<Hash>] eligible rows, as returned by `#eligible_rows`
384
- # @param field [Symbol] the field to average
385
- # @return [Object, Float, nil] the true middle value for an odd count, the
386
- # Float average of the two middle values for an even count, or `nil` for
387
- # an empty `rows`
388
- def recompute_median(rows, field)
389
- values = rows.map { |state| Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(state, field)) }
390
- .compact.sort
391
- return nil if values.empty?
392
-
393
- middle = values.length / 2
394
- values.length.odd? ? values[middle] : (values[middle - 1] + values[middle]) / 2.0
395
- end
396
- end
397
- end
398
- end
399
- end
@@ -1,137 +0,0 @@
1
- require_relative "../nondeterministic"
2
-
3
- module Hecks
4
- module Fuzzing
5
- module Properties
6
- # Lifecycle/saga-shape properties, and replay determinism itself: a
7
- # replayed history's lifecycle fields are among the aggregate's own
8
- # declared states, a saga's advances follow its declared handlers, and
9
- # replaying the same steps twice produces the same history.
10
-
11
- # Holds lifecycle_values_are_declared, saga_advances_follow_declared_handlers,
12
- # and replay_is_deterministic — the foundational property, since every
13
- # other property here trusts that a single replay's own history is
14
- # trustworthy in the first place.
15
- module LifecycleAndReplay
16
- # Every lifecycle field a replay leaves an instance holding is one
17
- # of the aggregate's own declared states — the full set, not just
18
- # `Lifecycle#states`' default+targets (see ModelCheck.full_states'
19
- # own comment on that hole). The tie to M2 is direct: the model
20
- # checker proves which states a domain's own declarations can ever
21
- # produce ; this proves a real run never produced anything else —
22
- # a coercion bug, a stale string surviving a rename, a default
23
- # that drifted from the declared set, would all show up here as a
24
- # value nothing upstream would have predicted.
25
- #
26
- # @param history [Hash] a replayed history, as returned by `Fuzzing::Replay.call`
27
- # @return [true, String] true if every instance's lifecycle field holds a declared
28
- # state; otherwise a semicolon-joined message naming each offending instance
29
- def lifecycle_values_are_declared(history)
30
- bluebook = history.fetch(:bluebook)
31
- declared = {}
32
- bluebook.aggregates.each do |aggregate|
33
- declared[aggregate.hecks_name] = Bluebook::ModelCheck.full_states(aggregate.lifecycle) if aggregate.lifecycle
34
- end
35
- return true if declared.empty?
36
-
37
- offenders = history.fetch(:instances).filter_map do |key, state|
38
- aggregate_name = key.split("::").last.split("#").first
39
- states = declared[aggregate_name]
40
- next unless states
41
-
42
- lifecycle = bluebook.aggregate(aggregate_name).lifecycle
43
- value = state[lifecycle.field]
44
- next if value.nil? || states.include?(value.to_s)
45
-
46
- "#{key} holds #{lifecycle.field}=#{value.inspect}, which #{aggregate_name} never declares as a state"
47
- end
48
-
49
- offenders.empty? || offenders.join("; ")
50
- end
51
-
52
- # Every saga advance a replay actually logged moved along an edge
53
- # the process manager declared — `(from, to)` pairs that appear in
54
- # `saga_log` with `advanced: true` must be a `(handler.from_state,
55
- # handler.to_state)` pair some handler on that PM declares
56
- # (compensation edges included ; a refused-triggered advance is a
57
- # handler like any other). A saga that advanced along a pair no
58
- # handler names would mean the runtime moved state the language
59
- # never authorized — the same trust ModelCheck's static reachability
60
- # rests on, checked here against what a run actually did.
61
- #
62
- # @param history [Hash] a replayed history as returned by `Replay.call`
63
- # @return [true, String] true if every logged advance matches a declared
64
- # handler edge; otherwise a message naming the process manager and pair
65
- # that does not
66
- def saga_advances_follow_declared_handlers(history)
67
- bluebook = history.fetch(:bluebook)
68
- edges = Hash.new { |h, k| h[k] = [] }
69
- bluebook.process_managers.each do |pm|
70
- pm.handlers.each { |handler| edges[pm.name] << [handler.from_state, handler.to_state] }
71
- end
72
- return true if edges.empty?
73
-
74
- offenders = history.fetch(:sagas).filter_map do |entry|
75
- next unless entry[:advanced]
76
-
77
- pair = [entry[:from], entry[:to]]
78
- next if edges[entry[:process_manager]].include?(pair)
79
-
80
- "#{entry[:process_manager]} advanced #{pair.inspect}, which no declared handler names"
81
- end
82
-
83
- offenders.empty? || offenders.join("; ")
84
- end
85
-
86
- # The foundational one. `Hecks::Runtime` mints nothing — every
87
- # identity is declared and derived, never invented (see
88
- # command_interpreter.rb's own "nothing is minted" — a random hex,
89
- # a counter, anything not reproducible from the payload, was
90
- # refused out of the runtime specifically because it broke this).
91
- # So the same steps, replayed against a fresh boot, must produce
92
- # byte-identical events, refusals, and instances — any drift here
93
- # is nondeterminism the runtime promised not to have: a wall-clock
94
- # read that leaked into compared state, a Hash iteration order a
95
- # comparison depended on, anything. Two independent replays, not a
96
- # cached one compared to itself, so a bug that corrupts the first
97
- # run's own bookkeeping cannot pass by agreeing with itself.
98
- #
99
- # @param domain_path [String] path to the domain directory to boot, such as
100
- # `"examples/pizzas"`
101
- # @param steps [Array<Hash>] the step list to replay twice
102
- # @param adapter [Symbol] persistence adapter to boot with (`:memory`,
103
- # `:postgres`, or `:postgres_era`)
104
- # @return [true, String] true if both replays produce identical histories
105
- # (after stripping declared nondeterministic fields); otherwise a message
106
- # naming the step count that diverged
107
- def replay_is_deterministic(domain_path, steps, adapter: :memory)
108
- first = Replay.call(domain_path, steps, adapter: adapter)
109
- second = Replay.call(domain_path, steps, adapter: adapter)
110
-
111
- # What leaves this comparison, and why, is declared once in
112
- # `Nondeterministic::FIELDS`: the `outbox_row` group (per-enqueue
113
- # uuids), the `event` group on each row's full `Event#to_h`
114
- # (`occurred_at`, which `history[:events]` never carried), and the
115
- # `history` group (live IR objects). Stripped per `outbox_traces`
116
- # row rather than dropping `outbox_traces` wholesale: everything
117
- # else on a row (status, consumer, kind, the event's own name/
118
- # aggregate/id/payload) is reproducible from the steps alone and
119
- # stays checked.
120
- strip_outbox_nondeterminism = lambda do |history|
121
- traces = Array(history[:outbox_traces]).map do |trace|
122
- trace.merge(rows: trace[:rows].map do |row|
123
- Nondeterministic.strip(row, :outbox_row).merge(event: Nondeterministic.strip(row[:event], :event))
124
- end)
125
- end
126
- history.merge(outbox_traces: traces)
127
- end
128
-
129
- comparable = ->(history) { Nondeterministic.strip(strip_outbox_nondeterminism.call(history), :history) }
130
- return true if comparable.call(first) == comparable.call(second)
131
-
132
- "two replays of the same #{steps.length} steps produced different histories"
133
- end
134
- end
135
- end
136
- end
137
- end