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
@@ -11,36 +11,22 @@ require_relative "value"
11
11
 
12
12
  module Hecks
13
13
  module Runtime
14
- # Answers one declared aggregate or entity query against a
15
- # repository: prefers a native adapter hook (Ports::Query.execute)
16
- # when the store can answer directly, falling back to interpreting
17
- # wheres/order_by/limit/offset over every loaded record itself.
18
- # #reference_call/#reference_interpret are a separate, deliberately
19
- # naive re-implementation of the same evaluation, used only as the
20
- # fuzzer's oracle to catch divergence between adapters and this
21
- # interpreter's own native path.
14
+ # Answers one declared aggregate or entity query against a repository, using a
15
+ # native adapter hook when the store has one and interpreting the query otherwise.
22
16
  class QueryInterpreter
23
17
  attr_reader :registry
24
18
 
25
- # @param registry [Runtime::Registry] the booted registry queries are answered
26
- # against
27
19
  def initialize(registry)
28
20
  @registry = registry
29
21
  end
30
22
 
31
23
  # Answers one declared aggregate or entity query, preferring a native adapter
32
- # hook and falling back to interpreting the query over every loaded record.
24
+ # hook and falling back to interpreting it over every loaded record.
33
25
  #
34
- # @param domain [String, Symbol] the domain `aggregate` belongs to
35
- # @param aggregate [Bluebook::Aggregate] the aggregate the query is declared on
36
- # @param query_name [String] the query's declared name, or an entity query's
37
- # dotted `"Entity.Query"` name
38
- # @param args [Hash{Symbol => Object}] the query's declared arguments
39
- # @return [Array<Hash>] one frozen Hash per matching record, its state with
40
- # `:id` merged in last
26
+ # @param query_name [String] a declared name, or an entity query's `"Entity.Query"`
27
+ # @return [Array<Hash>] one frozen Hash per matching record, `:id` merged in last
41
28
  # @raise [Runtime::UnknownVerb] if `query_name` names no declared query
42
- # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its
43
- # declared type
29
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its type
44
30
  # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
45
31
  def call(domain, aggregate, query_name, args)
46
32
  return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
@@ -48,67 +34,33 @@ module Hecks
48
34
  declared = declared_query(aggregate, query_name)
49
35
  args = normalize_args(aggregate, declared, args)
50
36
  declared = TenantScope.apply(declared, args)
51
- # After TenantScope, so its synthetic clause is already present
52
- # in `.wheres` and rides through as an ordinary local clause on
53
- # the outer query. It does not reach the hop's own inner
54
- # sub-query against the target aggregate — a hop's target may
55
- # not even declare the same tenant boundary, and propagating one
56
- # aggregate's tenant scope onto an unrelated aggregate's own
57
- # query is a real design question of its own, not answered here.
37
+ # Applied after TenantScope so its tenant clause rides through as an ordinary
38
+ # local clause; it is deliberately not propagated into the hop's inner query.
58
39
  declared = ReferenceHop.apply(declared, args, registry: @registry, domain: domain, aggregate: aggregate)
59
40
 
60
41
  repository = @registry.repository(domain, aggregate)
61
- # `registry:` is threaded through so a `none_in_state` where-clause
62
- # can look its target aggregate up — see
63
- # QuerySpecification::Common::Comparison#none_in_state? for the
64
- # comparator itself, and for why an ordinary aggregate-level
65
- # Memory query needs the registry passed explicitly rather than
66
- # closing over an instance variable.
42
+ # `registry:` lets a `none_in_state` where-clause look up its target aggregate.
67
43
  if (native = Ports::Query.execute(repository, declared, args,
68
44
  context: { domain: domain, aggregate: aggregate, registry: @registry }))
69
45
  records = native
70
- # `record.state.merge(id: record.id)` — id last, not first. See
71
- # Instance#to_h's own comment: an aggregate free to declare its
72
- # own attribute literally named `id` has that attribute's own
73
- # wrapped value sitting in `record.state[:id]` already; merging
74
- # it over a `{id:}.merge(state)` would let it silently
75
- # clobber the correct bare identity this row is supposed to
76
- # carry.
77
- # A query row is an answer, not a handle. Mutating one edits
78
- # nobody's state and silently disagrees with the store.
46
+ # `id` merges last so an aggregate attribute named `id` cannot clobber the
47
+ # bare identity (see Instance#to_h). Rows are frozen: a mutated row would
48
+ # edit nobody's state and disagree with the store.
79
49
  return Freezer.deep(records.map { |record| record.state.merge(id: record.id) })
80
50
  end
81
51
 
82
52
  Freezer.deep(interpret(repository.all, declared, args, domain: domain))
83
53
  end
84
54
 
85
- # The reference answer — this interpreter's own evaluation, never an
86
- # adapter's native hook. The fuzzer's query oracle replays every
87
- # generated ask through both paths and treats a difference as a
88
- # finding: the differential gate the retired cross-runtime harness
89
- # should always have been, aimed where the divergence actually
90
- # lives — between the engines inside this one runtime.
55
+ # The reference answer: this interpreter's own evaluation, never a native hook.
56
+ # The fuzzer's query oracle diffs it against #call.
91
57
  #
92
- # A hop clause is answered here by its own, deliberately naive
93
- # walk (reference_where_holds?) — never Runtime::ReferenceHop's
94
- # partition/fold/in-clause. Sharing that algorithm would have made
95
- # this oracle blind to exactly the code the hop feature adds: every
96
- # phase of a shared fold would still get diffed against the native
97
- # adapters, but the fold itself — the empty candidate set, a
98
- # duplicate id, a dangling reference, a chain's inside-out
99
- # resolution order — would only ever be compared against itself.
58
+ # Hop clauses use a deliberately naive walk (reference_where_holds?), not
59
+ # ReferenceHop's fold; sharing that would blind the oracle to it.
100
60
  #
101
- # @param domain [String, Symbol] the domain `aggregate` belongs to
102
- # @param aggregate [Bluebook::Aggregate] the aggregate the query is declared on
103
- # @param query_name [String] the query's declared name, or an entity query's
104
- # dotted `"Entity.Query"` name
105
- # @param args [Hash{Symbol => Object}] the query's declared arguments
106
- # @return [Array<Hash>] one Hash per matching record, its state with `:id`
107
- # merged in last
61
+ # @return [Array<Hash>] one Hash per matching record, `:id` merged in last
108
62
  # @raise [Runtime::UnknownVerb] if `query_name` names no declared query
109
- # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its
110
- # declared type
111
- # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
63
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its type
112
64
  def reference_call(domain, aggregate, query_name, args)
113
65
  return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
114
66
 
@@ -130,25 +82,15 @@ module Hecks
130
82
  def interpret(records, declared, args, domain: nil)
131
83
  matched = records.select { |r| declared.wheres.all? { |w| where_holds?(w, r, args, domain: domain) } }
132
84
  ordered = ordered(matched, declared.order_by, declared.null_semantics)
133
- # **Offset first, then limit** — the order SQL means by `LIMIT n
134
- # OFFSET m`, and the order Ports::Query::InMemory#execute already
135
- # applies (see that file's own comment). Without reading
136
- # `declared.offset`, offset would silently vanish for any query
137
- # answered here, not just come out reversed.
85
+ # Offset before limit, as SQL's `LIMIT n OFFSET m` and Ports::Query::InMemory do.
138
86
  skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
139
87
  capped = declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
140
88
 
141
- # id last — see the native-path comment above; same clobbering
142
- # risk for the in-memory reference interpreter's own rows.
143
89
  capped.map { |r| r.state.merge(id: r.id) }
144
90
  end
145
91
 
146
- # `interpret`'s own twin, for reference_call alone — same
147
- # select/order/limit shape, but a clause that hops through a
148
- # reference is answered by reference_where_holds? instead of the
149
- # plain FieldPath.dig(record, field) `where_holds?` uses (which
150
- # has no concept of a reference at all — it would just read the
151
- # raw id straight off the record and compare that).
92
+ # Twin of `interpret` for reference_call: hop clauses go through
93
+ # reference_where_holds?, which `where_holds?` cannot follow.
152
94
  def reference_interpret(records, declared, args, domain:, shape:)
153
95
  matched = records.select do |r|
154
96
  declared.wheres.all? do |w|
@@ -156,24 +98,14 @@ module Hecks
156
98
  end
157
99
  end
158
100
  ordered = ordered(matched, declared.order_by, declared.null_semantics)
159
- # **Offset first, then limit** — same fix, same reasoning, as
160
- # #interpret's own rows above.
161
101
  skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
162
102
  capped = declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
163
103
 
164
- # id last — same reasoning, same fix, as interpret's own rows.
165
104
  capped.map { |r| r.state.merge(id: r.id) }
166
105
  end
167
106
 
168
- # **The naive reading of a hop**: not a fold, not an id set — for
169
- # each candidate row, walk the reference by hand and dig the
170
- # field out of whatever it actually points at. A nil reference,
171
- # or one that resolves to nothing (a dangling id), makes the
172
- # whole clause false outright, whatever the comparator — "points
173
- # at a client that is not active" is false for a proposal with no
174
- # client at all, the same way it is false for one whose client
175
- # really is active; falling through to holds?(clause, nil, args)
176
- # instead would answer `ne` wrong (nil != "active" is true).
107
+ # Walks each reference by hand. A nil or dangling reference makes the whole clause
108
+ # false whatever the comparator; comparing nil directly would answer `ne` true.
177
109
  def reference_where_holds?(clause, record, args, domain:, shape:)
178
110
  step = QuerySpecification::HopPath.next_hop(clause.field, shape.attributes)
179
111
  return where_holds?(clause, record, args) unless step
@@ -205,21 +137,13 @@ module Hecks
205
137
 
206
138
  ordered = ordered_elements(rows, declared.order_by, declared.null_semantics,
207
139
  parent_key, entity.identity_heads)
208
- # **Offset first, then limit** — same fix, same reasoning, as
209
- # #interpret's own rows above. `entity_rows` is the only engine
210
- # for entity/sub-list queries, so a declared offset here silently
211
- # vanished for every entity query, not merely one path among
212
- # several.
140
+ # Entity queries have no native path, so offset must be applied here.
213
141
  skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
214
142
  declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
215
143
  end
216
144
 
217
- # The three declarations `entity_rows` needs before it can read a
218
- # single record — the entity itself, its declared query, and the
219
- # list attribute that holds it on the aggregate. Extracted from
220
- # `entity_rows` (pure extraction, same lookups, same order, same
221
- # UnknownVerb refusals) purely to separate "which declarations does
222
- # this dotted name resolve to" from the row-computation that follows.
145
+ # Resolves a dotted name to the entity, its declared query and the list attribute
146
+ # holding it, refusing with UnknownVerb when any is missing.
223
147
  def resolve_entity_query(aggregate, entity_name, query_name)
224
148
  entity = aggregate.entities.find { |piece| piece.hecks_name == entity_name } ||
225
149
  raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_unknown",
@@ -233,35 +157,19 @@ module Hecks
233
157
  [entity, declared, list_attr]
234
158
  end
235
159
 
236
- # FieldPath.dig, not a raw `element[clause.field.to_sym]` — a dotted
237
- # `where` (`where "price.cents" < 100`) needs the same segment-by-
238
- # segment walk every other query path already gets. Reading the whole
239
- # dotted string as one key always missed — `element[:"price.cents"]`
240
- # is never a real key — so a dotted where on an entity query
241
- # silently matched nothing, on the only engine entity queries have.
160
+ # FieldPath.dig, not `element[clause.field.to_sym]`: a dotted field such as
161
+ # "price.cents" is never a single key.
242
162
  def element_where_holds?(clause, element, args)
243
163
  holds?(clause, QuerySpecification::FieldPath.dig(element, clause.field), args)
244
164
  end
245
165
 
246
- # A row's own key. A sub-list row is a hydrated entity element (symbol-keyed
247
- # since every adapter decodes through `Ports::Persistence::StateCodec` and
248
- # `EntityListCoercion#hydrate_entity_list` symbolizes each element — PR A4
249
- # removed the string-spelling fallback that coped with the old per-adapter
250
- # shapes). It rides `comparable` for the same reason a where-clause does : an
251
- # identity is a value object, and `to_s` on one is an object address — a sort key
252
- # that differs run to run, which is worse than the store order it replaced.
166
+ # Sub-list rows are symbol-keyed because every adapter decodes through
167
+ # `Ports::Persistence::StateCodec`.
253
168
  def cell(row, key) = row[key.to_sym]
254
169
 
255
- # A sub-list row is identified by its parent and then its own key : two
256
- # entities under different parents can share a sequence, so the parent has
257
- # to lead or the tie is not broken at all.
258
- #
259
- # Every key the piece is known by, in declaration order, for the same
260
- # reason the parent leads: a part that ties is a part that breaks no tie.
261
- # This took `identified_by`, which is the single head and is nil the
262
- # moment an identity has two parts — and `cell(row, nil)` calls
263
- # `nil.to_sym`, so a query against a composite piece did not sort wrongly,
264
- # it raised. A piece known by one key sorts exactly as it did.
170
+ # Orders by parent first, then every key of the piece in declaration order: two
171
+ # entities under different parents can share a sequence, and a composite identity
172
+ # has no single head (`cell(row, nil)` would raise).
265
173
  def ordered_elements(rows, order_by, null_semantics, parent_key, entity_keys)
266
174
  field = order_by&.field
267
175
  Ports::Query::Ordering.apply(
@@ -276,13 +184,8 @@ module Hecks
276
184
  holds?(clause, QuerySpecification::FieldPath.dig(record, clause.field), args, record: record, domain: domain)
277
185
  end
278
186
 
279
- # The comparator table itself lives in
280
- # QuerySpecification::Common::Comparison, shared with
281
- # Ports::Query::InMemory#holds? rather than each carrying its own
282
- # copy — two copies once drifted: `none_in_state` reached only one
283
- # of them, and `comparable` disagreed about value objects with two
284
- # numeric members. What stays here is how a value is reached for
285
- # this path: the registry is instance state rather than an argument.
187
+ # The comparator table is shared with Ports::Query::InMemory#holds? through
188
+ # QuerySpecification::Common::Comparison so the two cannot drift.
286
189
  def holds?(clause, held, args, record: nil, domain: nil)
287
190
  QuerySpecification::Common::Comparison.holds?(
288
191
  clause.op, comparable(held), comparable(resolve_query_value(clause.value, args)), registry: @registry
@@ -293,49 +196,14 @@ module Hecks
293
196
  value.is_a?(Symbol) ? args[value] : value
294
197
  end
295
198
 
296
- # `boundary: false` always (C3.8 — a query's declared argument types
297
- # name the argument for callers and generators, never a runtime
298
- # shape checked here). A null required value-object-typed query
299
- # argument, though, is not a runtime-shape question at all: C3.7
300
- # says a named query's declared value-object arguments are checked
301
- # the same way a command argument's own is, so a `nil` offered for
302
- # a non-optional value-object-typed query attribute
303
- # (Governance::RoleAssignment.AssignmentsForActor's `actor_id`, say)
304
- # has to refuse — passing it through unchecked would let it through
305
- # as a silent, unfiltered query instead, a real
306
- # Ruby/Rust divergence the fuzzer caught (QualityControl BUG#2).
307
- #
308
- # `checked_vo?` true is handled by `null_vo_argument!` directly,
309
- # never by routing through `Value.for_attribute(argument: true)`
310
- # into the shared `Value::Coercion#nil_argument` the command door
311
- # (`Interpreting#coerce_declared_arguments`) still uses — that
312
- # method builds a null value object from zero fields, which
313
- # succeeds (silently absorbing the null via the type's own field
314
- # defaults) whenever every field happens to have one
315
- # (`Lease.Expired`'s `now`, a `LeaseInstant` with a `default: 0`
316
- # field; `Account.Overdrawn`/`HighBalance`/`StrictlyAbove`/
317
- # `AtMost`'s `floor`/`cap`, a two-defaulted-field `Money`) and only
318
- # refuses when a field has none (`Order.CostingLessThan`'s
319
- # `ceiling`, a defaultless `Price`) — a real query-side divergence
320
- # from Rust, which always refuses `TypeMismatch` on an explicit
321
- # null argument regardless of any default (QualityControl BUG#36).
322
- # `null_vo_argument!` instead treats an explicit null exactly the
323
- # way `Value.fields_for` already treats any other wrong-shaped
324
- # (non-Hash, non-Value) value offered for that same attribute — a
325
- # single-field value object auto-wraps into `{field: nil}` (whose
326
- # own `nil` is a present key, so `Value.build`'s `apply_defaults`
327
- # never fills it, and `check_required_fields` refuses it exactly
328
- # as any other missing required field would); a multi-field value
329
- # object refuses immediately with the same `value_object_shape`
330
- # wording an ordinary wrong-shaped scalar already gets. Command
331
- # arguments are deliberately untouched — this is a query-only
332
- # door; `nil_argument`'s own default-absorbing fallback still
333
- # governs a null command argument exactly as it always has.
199
+ # Coerces with `boundary: false`: a query's declared argument types are not a
200
+ # runtime shape check. The exception is a nil for a required value-object-typed
201
+ # argument, which must refuse as a command argument does; passing it through
202
+ # would run an unfiltered query (a Ruby/Rust divergence).
334
203
  #
335
- # C3.8's own bare-scalar carve-out stays intact: `checked_vo?` is
336
- # only true for a value-object-typed attribute, so a bare
337
- # `String`/`Integer` query argument offered nil still passes
338
- # through exactly as it always did.
204
+ # It does not use `Value::Coercion#nil_argument`, which builds a null value object
205
+ # from field defaults and only refuses when a field has none. `null_vo_argument!`
206
+ # refuses like any other wrong-shaped value. Command arguments are untouched.
339
207
  def normalize_args(aggregate, declared, args)
340
208
  declared.attributes.each_with_object(args.dup) do |attribute, normalized|
341
209
  next unless normalized.key?(attribute.name)
@@ -357,10 +225,7 @@ module Hecks
357
225
  !Value.value_object_for(aggregate, attribute.type).nil?
358
226
  end
359
227
 
360
- # Only ever reached when `checked_vo?` has already confirmed the
361
- # attribute's type resolves to a real value object — see its own
362
- # comment above for why this refuses unconditionally, never
363
- # absorbing the null via the type's own field defaults.
228
+ # Refuses a nil for a value-object argument; never absorbs it via field defaults.
364
229
  def null_vo_argument!(aggregate, attribute)
365
230
  value_object = Value.value_object_for(aggregate, attribute.type)
366
231
  Value.build(value_object, Value.fields_for(value_object, attribute.name, nil), aggregate)
@@ -368,15 +233,8 @@ module Hecks
368
233
 
369
234
  def comparable(value) = QuerySpecification::Common::Comparison.comparable(value)
370
235
 
371
- # FieldPath.dig, not a raw `record[field]` — `record` is an Instance
372
- # here, and a dotted order_by (`order_by "price.cents"`) is a
373
- # single symbol key (`:"price.cents"`) that never matches anything
374
- # `Instance#[]` actually holds, so a dotted order_by silently sorted
375
- # by all-nil (the identity tier alone deciding every tie) on this,
376
- # the reference/no-native-hook engine — the same bug already fixed
377
- # for `where` (see `where_holds?` above) and for entity rows (see
378
- # `ordered_elements` below), just not yet for this, the aggregate-
379
- # level order_by.
236
+ # FieldPath.dig, not `record[field]`: a dotted order_by is one symbol key that
237
+ # `Instance#[]` never holds, so it would sort by all-nil.
380
238
  def ordered(records, order_by, null_semantics = nil)
381
239
  field = order_by&.field
382
240
  Ports::Query::Ordering.apply(records, order_by, null_semantics,
@@ -5,27 +5,19 @@ require_relative "value"
5
5
 
6
6
  module Hecks
7
7
  module Runtime
8
- # Turns facts selected by a policy or process manager into the same
9
- # receiver/payload envelope an outside caller uses. A reaction declares
10
- # both receiver identities and facts through one `with:` map, so this is
11
- # the one compatibility seam that separates them after resolving the
12
- # declaration and before re-entering the dispatcher.
8
+ # Turns facts a policy or process manager selected into the receiver/payload
9
+ # envelope an outside caller uses.
13
10
  module ReactionInvocation
14
11
  Target = Struct.new(:aggregate, :entities, :command, keyword_init: true)
15
12
  # :facts, not :values — Struct.new already defines #values (every
16
- # member's own value, in order); naming this field :values silently
17
- # shadowed that with the single Hash it actually holds.
13
+ # member's own value, in order); naming this field :values would
14
+ # silently shadow that with the single Hash it actually holds.
18
15
  Scope = Struct.new(:name, :facts, keyword_init: true)
19
16
 
20
17
  module_function
21
18
 
22
- # Reports whether a reaction declared an explicit `with:` projection.
23
- #
24
- # An omitted projection and an explicitly empty `with: {}` both hold
25
- # as the same empty array on the holding IR. Builders preserve
26
- # declaration presence off-wire, in `@projection_declared`;
27
- # reconstructed/legacy IR without that ivar falls back to reading
28
- # presence off whether `with_spec` is non-empty.
19
+ # Reports whether a reaction declared an explicit `with:` projection;
20
+ # an omitted projection and an explicit `with: {}` both count as none.
29
21
  #
30
22
  # @param declaration [Bluebook::Policy, Bluebook::DispatchSpec] the reacting
31
23
  # declaration to check
@@ -38,27 +30,16 @@ module Hecks
38
30
  end
39
31
  end
40
32
 
41
- # Resolves a declared `with:` projection against the scopes and bindings visible to it.
42
- #
43
- # A reaction's source names resolve lexically, not globally. Policies
44
- # supply one event/row scope. Process managers supply current event then
45
- # opening-event memory, while correlation is an explicit binding ahead
46
- # of both. Missing names are refused here rather than materialized as nil
47
- # and accidentally presented as target command facts.
33
+ # Resolves a declared `with:` projection against the scopes and bindings visible
34
+ # to it, refusing a name resolved in neither rather than passing it as nil.
48
35
  #
49
- # @param with_spec [Hash{Symbol => Object}] each target fact name mapped to its source:
50
- # a Symbol naming a fact visible in `bindings` or `scopes`, or any other value taken
51
- # as a literal
52
- # @param scopes [Array<Hecks::Runtime::ReactionInvocation::Scope, Array(String,
53
- # Hash)>] the named fact scopes to resolve a Symbol source against, checked in order;
54
- # a bare `[name, facts]` pair is wrapped into a `Scope`
55
- # @param bindings [Hash] explicit bindings (such as a saga's correlation key), checked
56
- # before any scope
36
+ # @param with_spec [Hash] fact name => source: a Symbol looked up in `bindings`/`scopes`,
37
+ # or any other value taken as a literal
38
+ # @param scopes [Array<Scope, Array(String, Hash)>] named fact scopes, checked in order
39
+ # @param bindings [Hash] explicit bindings (e.g. a saga's correlation key), checked first
57
40
  # @param label [String] names this resolution in an `UnknownArgument` refusal
58
- # @return [Hash{Symbol => Object}] `with_spec`'s keys mapped to their resolved,
59
- # materialized values
60
- # @raise [Runtime::UnknownArgument] if a Symbol source names a fact visible in no
61
- # binding and no scope
41
+ # @return [Hash] `with_spec`'s keys mapped to their resolved, materialized values
42
+ # @raise [Runtime::UnknownArgument] if a Symbol source is visible in no binding or scope
62
43
  def resolve_mapping(with_spec:, scopes:, bindings: {}, label: "reaction")
63
44
  normalized_bindings = bindings.transform_keys(&:to_sym)
64
45
  normalized_scopes = scopes.map do |scope|
@@ -72,10 +53,6 @@ module Hecks
72
53
  end
73
54
  end
74
55
 
75
- # One `with:` source, resolved — pulled out of resolve_mapping
76
- # because it is a pure function of its own arguments (a literal, a
77
- # binding, or a name visible in some scope), with no dependency on
78
- # anything else resolve_mapping's own to_h block is doing.
79
56
  def resolved_mapping_value(source, bindings, scopes, label)
80
57
  return source unless source.is_a?(Symbol)
81
58
  return bindings.fetch(source) if bindings.key?(source)
@@ -83,13 +60,7 @@ module Hecks
83
60
  visible = scopes.find { |scope| scope.facts.key?(source) }
84
61
  unless visible
85
62
  names = scopes.map(&:name).join(" then ")
86
- # **What is visible, named**. A refusal that only says which name is
87
- # missing sent a modeler guessing field after field
88
- # (`number`, `reference`…) at a fan-out row that is addressed
89
- # by one key — `account`, the lowercase aggregate — which
90
- # nothing else in the domain spells out. The names each scope
91
- # actually offers are the whole diagnosis; the refusal now
92
- # lists them, scope by scope.
63
+ # Names every visible option so a caller isn't left guessing at fields.
93
64
  offered = scopes.map { |scope| "#{scope.name}: #{scope.facts.keys.sort.join(', ')}" }.join("; ")
94
65
  raise UnknownArgument,
95
66
  "#{label}'s with: reads :#{source}, which is not visible in #{names} (visible — #{offered})"
@@ -98,39 +69,18 @@ module Hecks
98
69
  end
99
70
  private_class_method :resolved_mapping_value
100
71
 
101
- # A reaction without an explicit projection retains wholesale legacy
102
- # forwarding. Choosing `with:` opts into the strict envelope: only target
103
- # command attributes enter `with:`, while identities become `to:`.
104
- # `consumed` accumulates across the aggregate-identity and entity-
105
- # identity steps below, and refuse_unconsumed! at the end reads the
106
- # final list — an ordering dependency threaded through one shared
107
- # local. Already leans on private helpers (identity_for,
108
- # source_receiver_for, command_facts, refuse_unconsumed!) for every
109
- # piece that is self-contained; what remains is the sequencing
110
- # itself, which further splitting would only relocate, not remove.
72
+ # A reaction without an explicit `with:` projection forwards its payload
73
+ # wholesale; an explicit one builds a strict `{to:, with:}` envelope instead.
111
74
  # rubocop:disable-next Metrics/MethodLength, Metrics/PerceivedComplexity
112
- #
113
- # @param registry [Runtime::Registry] the booted registry to resolve `verb` against
114
75
  # @param verb [String] the fully qualified target command verb
115
- # @param projected [Hash] the facts to send, already resolved (e.g. by
116
- # `resolve_mapping`) or, for a legacy reaction, the raw event/row payload
117
- # @param explicit [Boolean] true when the reaction declared its own `with:` projection
118
- # (`projection_declared?`); false forwards `projected` wholesale as legacy args
119
- # @param passthrough [Array<String, Symbol>] extra fact names allowed to ride along
120
- # unconsumed, beyond the receiver identity and declared command facts
121
- # @param source_receiver [Hash{Symbol => Object}, nil] the triggering event's own
122
- # `{aggregate:, identity:}`, offered as a same-aggregate receiver when nothing else
123
- # supplies one; nil when there is no such event to inherit from
124
- # @return [Hash{Symbol => Object}] `{to:, with:}` for an explicit projection targeting
125
- # a non-creating command (`with:` only for a creating command); otherwise `projected`
126
- # (with `to:` merged in when a receiver could be inherited)
127
- # @raise [Runtime::UnknownVerb] if `verb` does not resolve to a declared command,
128
- # entity command, or port operation (only when `explicit` is true; a legacy call
129
- # resolving `verb` only to check inheritance swallows this and forwards unchanged)
130
- # @raise [Runtime::TypeMismatch] if an explicit projection resolves no receiver
131
- # identity for the target aggregate or one of its entities
132
- # @raise [Runtime::UnknownArgument] if an explicit projection's facts include a name
133
- # that is neither a consumed receiver identity nor a declared command fact
76
+ # @param projected [Hash] resolved facts, or the raw payload when not `explicit`
77
+ # @param explicit [Boolean] whether the reaction declared its own `with:` projection
78
+ # @param passthrough [Array<String, Symbol>] extra fact names allowed unconsumed
79
+ # @param source_receiver [Hash, nil] an inheritable receiver from the triggering event
80
+ # @return [Hash] `{to:, with:}` when explicit, else `projected` merged with `to:`
81
+ # @raise [Runtime::UnknownVerb] if `verb` does not resolve (only when `explicit`)
82
+ # @raise [Runtime::TypeMismatch] if no receiver identity resolves for an explicit target
83
+ # @raise [Runtime::UnknownArgument] if an explicit projection has an undeclared fact
134
84
  def build(registry:, verb:, projected:, explicit:, passthrough: [], source_receiver: nil)
135
85
  args = projected.transform_keys(&:to_sym)
136
86
  unless explicit
@@ -144,16 +94,9 @@ module Hecks
144
94
  rescue UnknownVerb
145
95
  return args
146
96
  end
147
- # An entity target has no shape this branch can build — an
148
- # implicit (unprojected) `to:` is always a bare scalar, the
149
- # aggregate's own identity alone; an entity command's receiver is
150
- # `{aggregate:, entities:}`, which needs the entity's own
151
- # identity too, and nothing here resolves one (the same "cannot
152
- # invent an entity identity" limit `source_receiver_for`'s own
153
- # comment already states). So this stays exactly the pre-BUG#6
154
- # behavior for an entity target: no receiver is lifted, and the
155
- # payload forwards wholesale, unchanged, same as when there is
156
- # no `source_receiver` at all.
97
+ # An entity target's receiver needs `{aggregate:, entities:}`, but nothing
98
+ # here can invent the entity's own identity — so no receiver is lifted here;
99
+ # the payload forwards wholesale, same as with no `source_receiver` at all.
157
100
  return args unless target.entities.empty?
158
101
 
159
102
  inherited_receiver = source_receiver_for(target, source_receiver)
@@ -212,18 +155,9 @@ module Hecks
212
155
 
213
156
  *entity_names, command_name = command_path.split(".")
214
157
 
215
- # A port operation, reached by the same two-segment tail shape an
216
- # entity command uses ("Head.Rest") — checked first, same order
217
- # `Dispatcher#dispatch` already resolves a live verb in (an
218
- # aggregate that declared both a port and an entity of the same
219
- # name would resolve to the port there too; no domain in this
220
- # corpus does). A port has no `entities` of its own — the
221
- # receiver is always the aggregate itself — so `Target#entities`
222
- # stays empty and `#command` holds the `PortOperation`, which
223
- # answers `#creates?` (always false) the same way an ordinary
224
- # `Command` does, letting `source_receiver_for` lift a same-
225
- # aggregate policy's own Event.id as the operation's receiver
226
- # exactly as it already does for a plain command.
158
+ # A port operation shares the two-segment tail shape an entity command
159
+ # uses ("Head.Rest"); checked first, matching the order
160
+ # `Dispatcher#dispatch` already resolves a live verb in.
227
161
  if entity_names.one? && (port = aggregate.port(entity_names.first))
228
162
  operation = port.operation(command_name)
229
163
  raise UnknownVerb, "reaction target #{verb.inspect} does not resolve to a declared port operation" unless operation
@@ -259,49 +193,13 @@ module Hecks
259
193
  end
260
194
  private_class_method :aggregate_aliases
261
195
 
262
- # Event.id names the aggregate that emitted the event. It can therefore
263
- # supply only the receiver of a non-creating command on that same root;
264
- # it cannot address another aggregate, invent an entity identity, or turn
265
- # a creation into a mutation. An explicit projected receiver is resolved
266
- # first and remains authoritative.
267
- #
268
- # Answers regardless of `target.entities` — the value this method hands
269
- # back is only ever the root aggregate's own identity (`source_receiver`
270
- # carries nothing else: `event.aggregate`/`event.id` name the emitting
271
- # aggregate, never one of its entities). An entity target's own identity
272
- # is resolved entirely separately, from `args` (`build`'s own `entity_
273
- # identities` loop) — this method is never consulted for it, so
274
- # answering for an entity target does not "invent an entity identity"
275
- # any more than answering for a plain one invents that identity; it was
276
- # already the one thing this method has ever supplied. `build`'s own
277
- # explicit branch (below) already applies this correctly either way
278
- # (`aggregate_identity ||= inherited_receiver`, entity identities read
279
- # from `args` regardless) — the caller that actually needed a guard
280
- # here is the implicit one, above, which has no shape to build a
281
- # `{aggregate:, entities:}` receiver from an inherited scalar alone,
282
- # and now guards itself for exactly that reason instead of this method
283
- # doing it on that caller's behalf and, as a side effect, also refusing
284
- # the explicit caller's own legitimate case — confirmed missing until
285
- # `qa/stress_domains/waybill` (BUG#6) exercised it for the first time
286
- # anywhere in the corpus: a saga dispatching into a nested entity's own
287
- # command, inheriting its aggregate receiver from the event that
288
- # triggered it.
196
+ # An event's own identity can supply the receiver of a non-creating command on
197
+ # the same aggregate root; it never addresses another aggregate or an entity.
289
198
  def source_receiver_for(target, source_receiver)
290
199
  return nil unless source_receiver
291
- # `target.command.creates?` alone misreads every entity command —
292
- # `Behaviour::Command#creates?`'s own comment: "a verb declared on
293
- # an entity always acts on that piece... which means `creates?`
294
- # answers true for every one of them" (it never sets `@references`
295
- # the way an aggregate-level command's own `reference_to` does,
296
- # not because it brings anything new into being). `build`'s own
297
- # `if target.entities.empty? && target.command.creates?` guard,
298
- # 20-odd lines below, already reads `creates?` correctly for
299
- # exactly this reason — an entity command is never a genuine
300
- # creation, whatever `creates?` alone answers — so this checks the
301
- # same compound condition instead of the bare, misleading half of
302
- # it. Left unfixed, an entity target's own receiver was refused
303
- # here even after this method stopped refusing on `target.entities`
304
- # alone — the second half of BUG#6's own fix.
200
+ # `target.command.creates?` alone misreads every entity command, which always
201
+ # answers true for `creates?` though it never creates anything (Behaviour::
202
+ # Command#creates?) — checked as the same compound condition `build` uses below.
305
203
  return nil if target.entities.empty? && target.command.creates?
306
204
 
307
205
  source = source_receiver.transform_keys(&:to_sym)