hecks 2.8.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 (404) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +4 -6
  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 +17 -98
  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 +5 -48
  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 +31 -141
  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 +14 -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 +25 -31
  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 +16 -69
  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 -194
  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 -111
  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 +53 -267
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +43 -305
  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 +6 -21
  114. data/lib/hecks/bluebook/model_check.rb +100 -597
  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 +4 -23
  128. data/lib/hecks/cli/document.rb +3 -21
  129. data/lib/hecks/cli/ir.rb +3 -5
  130. data/lib/hecks/cli/mcp.rb +2 -3
  131. data/lib/hecks/cli/mcp_door.rb +19 -114
  132. data/lib/hecks/cli/model_check.rb +18 -85
  133. data/lib/hecks/cli/project_cli.rb +7 -40
  134. data/lib/hecks/cli/project_diagrams.rb +0 -7
  135. data/lib/hecks/cli/run.rb +12 -58
  136. data/lib/hecks/cli/smoke_test.rb +2 -16
  137. data/lib/hecks/cli/stores.rb +1 -2
  138. data/lib/hecks/cli.rb +18 -24
  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 +82 -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 +9 -90
  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 +61 -64
  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 +6 -25
  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 +18 -124
  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 +150 -678
  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 +2 -15
  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 +42 -134
  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 +3 -6
  336. data/lib/hecks/release/runner/commands.rb +2 -4
  337. data/lib/hecks/release/runner/gem_publisher.rb +2 -3
  338. data/lib/hecks/release/runner/npm_publisher.rb +6 -20
  339. data/lib/hecks/release/runner/preflight.rb +4 -19
  340. data/lib/hecks/release/runner/tagger.rb +2 -9
  341. data/lib/hecks/release/runner.rb +3 -32
  342. data/lib/hecks/rendering.rb +4 -26
  343. data/lib/hecks/router/namespace_installer.rb +12 -36
  344. data/lib/hecks/router.rb +4 -8
  345. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  346. data/lib/hecks/runtime/boot_gates.rb +3 -18
  347. data/lib/hecks/runtime/caller.rb +19 -59
  348. data/lib/hecks/runtime/capability_graph.rb +5 -21
  349. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  350. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  351. data/lib/hecks/runtime/command_interpreter.rb +124 -306
  352. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  353. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  354. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  355. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  356. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  357. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  358. data/lib/hecks/runtime/dispatcher.rb +82 -241
  359. data/lib/hecks/runtime/entity_element.rb +40 -462
  360. data/lib/hecks/runtime/entity_interpreter.rb +64 -219
  361. data/lib/hecks/runtime/errors.rb +32 -115
  362. data/lib/hecks/runtime/event.rb +3 -25
  363. data/lib/hecks/runtime/identity.rb +18 -154
  364. data/lib/hecks/runtime/instance.rb +18 -150
  365. data/lib/hecks/runtime/interpreting.rb +11 -70
  366. data/lib/hecks/runtime/invocation.rb +28 -164
  367. data/lib/hecks/runtime/loader.rb +48 -186
  368. data/lib/hecks/runtime/outbox.rb +38 -261
  369. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  370. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  371. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  372. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  373. data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
  374. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  375. data/lib/hecks/runtime/reference_hop.rb +8 -81
  376. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  377. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  378. data/lib/hecks/runtime/registry/verification.rb +52 -225
  379. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  380. data/lib/hecks/runtime/registry.rb +99 -244
  381. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  382. data/lib/hecks/runtime/routing.rb +4 -15
  383. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  384. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  385. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  386. data/lib/hecks/runtime/tenant_check.rb +6 -68
  387. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  388. data/lib/hecks/runtime/value/admission.rb +16 -67
  389. data/lib/hecks/runtime/value/coercion.rb +155 -641
  390. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  391. data/lib/hecks/runtime/value.rb +12 -84
  392. data/lib/hecks/runtime.rb +17 -65
  393. data/lib/hecks/storehouse.rb +141 -568
  394. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  395. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  396. data/lib/hecks/vendoring/git_environment.rb +2 -8
  397. data/lib/hecks/vendoring/git_source.rb +4 -10
  398. data/lib/hecks/vendoring.rb +7 -46
  399. data/lib/hecks/version.rb +3 -14
  400. data/lib/hecks.rb +26 -81
  401. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  402. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  403. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  404. metadata +3 -2
@@ -11,69 +11,21 @@ module Hecks
11
11
  # Whether a command may run at all: its declared givens, and the
12
12
  # lifecycle transition it asks for.
13
13
  module Admissibility
14
- # Wraps `subject` so a guard can read a declared-but-storage-absent
15
- # optional attribute as nil, not "cannot resolve." `Instance
16
- # #hydrate_with_defaults` deliberately leaves one absent rather
17
- # than nil-filled — "an attribute with no default stays absent,
18
- # exactly as stored," that file's own comment — which is right
19
- # for storage fidelity but wrong for a `given`/`ensures` reading
20
- # an optional field a record predates (measured, not assumed: a
21
- # real Item.Promote crashed on `!promoted` against a record from
22
- # before `promoted` existed). Defensive on purpose — a `subject`
23
- # with no `.aggregate` (an entity's own view/settled wrapper,
24
- # entity_interpreter.rb's own callers) just degrades to today's
25
- # exact behavior, zero risk to a path this bug was never measured
26
- # against.
27
- #
28
- # NIL is not the only outcome, though. The Item.Promote fix above
29
- # covered an optional field — the honest case, where absence is
30
- # exactly what optional means. It also, as a side effect, let a
31
- # non-optional field predating a record read nil the same way,
32
- # which is the `ne:`/array-`in:` bug class applied to rule
33
- # evaluation: a predicate silently answers against a value nobody
34
- # ever wrote (ADR 0025, "Added attributes and absence"). `[]`
35
- # below narrows the nil-read back to what it was built for —
36
- # optional stays nil, non-optional raises a named refusal
37
- # identifying the field, so a rule that reads it fails loud
38
- # instead of quietly wrong.
14
+ # Wraps `subject` so a guard reads a declared, optional attribute
15
+ # that predates the record as nil instead of raising.
39
16
  class GuardState
40
- # @param instance [Runtime::Instance] the record a `given`/`ensures`/
41
- # `invariant` rule reads state from
42
17
  def initialize(instance)
43
18
  @instance = instance
44
19
  @declared = instance.respond_to?(:aggregate) ? instance.aggregate.attributes.to_h { |a| [a.name, a] } : {}
45
- # S12, ADR 0025 — a separate index, the same reason
46
- # `Aggregate#projected_fields` is a separate IR collection
47
- # rather than folded into `attributes` (see that field's
48
- # own comment): a projected field's absence means
49
- # something different from an ordinary attribute's, so it
50
- # needs its own refusal below, not `AttributeAbsent`'s.
51
- # `projects` is aggregate-scoped only — an entity's own
52
- # `instance.aggregate` answers the entity construct here
53
- # (EntityInterpreter's own subject), which declares no
54
- # `projected_fields` of its own, hence the extra guard
55
- # `@declared` above does not need.
20
+ # Separate index: a projected field's absence raises its own
21
+ # refusal, not AttributeAbsent's (ADR 0025).
56
22
  owner = instance.aggregate if instance.respond_to?(:aggregate)
57
23
  @projected = owner.respond_to?(:projected_fields) ? owner.projected_fields.to_h { |f| [f.name, f] } : {}
58
24
  end
59
25
 
60
- # Reports whether `name` is a declared attribute, a projected field, or
61
- # a key `instance`'s own state actually holds.
62
- #
63
- # @param name [String, Symbol] the field name to check
64
- # @return [Boolean] true if `name` is readable one of those three ways
65
26
  def key?(name) = @declared.key?(name.to_sym) || @projected.key?(name.to_sym) || @instance.key?(name)
66
27
 
67
- # Reads one field, the way a `given`/`ensures`/`invariant` rule expects
68
- # absence to read.
69
- #
70
- # @param name [String, Symbol] the field name to read
71
- # @return [Object, nil] the stored value; nil for a declared, optional
72
- # attribute the record predates
73
- # @raise [Runtime::AttributeAbsent] if `name` is a declared, non-optional
74
- # attribute the record's own state does not hold
75
- # @raise [Runtime::ProjectionAbsent] if `name` is a `projects` field the
76
- # record's own state does not hold
28
+ # Nil for an absent optional attribute; raises for anything else absent.
77
29
  def [](name)
78
30
  return @instance[name] if @instance.key?(name)
79
31
 
@@ -99,83 +51,23 @@ module Hecks
99
51
  end
100
52
  private_constant :GuardState
101
53
 
102
- # `domain:` is only needed to dereference a reference-typed field
103
- # (`customer.status`) — see References#dereference. `owner` is the
104
- # declaring aggregate/entity when `subject` carries one (an
105
- # entity's pre-mutation `view` does, same as an aggregate's
106
- # `Instance`); a `subject` with no `.aggregate` just hydrates
107
- # nothing from state, same as GuardState degrades above.
108
- #
109
- # Merge order matters, and it is not "args always win": an
110
- # unaliased command-level reference dereferences under a
111
- # different name than the argument holds (`account_id` the arg,
112
- # `account` the hydrated key — no collision, order is moot). An
113
- # aliased one (`reference_to Customer, as: :customer`) hydrates
114
- # under the same name the argument itself holds — `customer` is
115
- # both the raw id an arg puts there and the key `customer.status`
116
- # expects to dig into. If `args` merged last, the raw id (a
117
- # String) would win and `.status` on a String is where a fuzzer
118
- # found this — TypeError, not a refusal. Command-level
119
- # dereferencing is the one thing that is supposed to override
120
- # its own source argument for exactly this reason; args still
121
- # wins over stored owner state.
122
- # `parent:` is an entity command's own parent aggregate record
123
- # (EntityInterpreter's `ctx.instance` — "the parent aggregate
124
- # record", its own doc comment) — the entity's containment, not a
125
- # declared reference attribute, so it doesn't come from
126
- # `dereference`'s attribute scan the way `owner`'s do. Hydrated
127
- # the same shape regardless: the parent's own state, merged so
128
- # the dereferenced hash wins over the raw reference it replaces
129
- # (ADR 0025 drops the `_id` suffix that once kept the two
130
- # apart by name, so `parent.state.merge(dereference(...))` is
131
- # load-bearing, not redundant) — plus its own references
132
- # dereferenced one level in, so `parent.customer.status` (a
133
- # parent aggregate reaching its own customer) resolves the same
134
- # way `account.customer.status` does for a command-level reference.
135
- # nil for an aggregate command — CommandInterpreter never passes it.
136
- # `correction:` — the `{as_name => payload}` bindings
137
- # `enforce_correction_target` (above) already located, merged in
138
- # last so an `as:` name wins the same way `old:` always wins in
139
- # `enforce_ensures`, below — it is a fresh local binding a
140
- # `corrects` command introduces, not a real argument/state field
141
- # a caller could collide with by accident.
54
+ # Checks whether `command` may run against `subject`: its declared
55
+ # `given`s, then `declaring`'s lifecycle guard.
142
56
  #
143
- # @param subject [Runtime::Instance] the pre-mutation record a `given` is
144
- # checked against
145
- # @param command [Class] the command class (`Bluebook::Command` subclass)
146
- # whose declared `givens` are checked
147
- # @param args [Hash{String, Symbol => Object}] the offered command arguments,
148
- # readable by a given
149
- # @param domain [String, Symbol] the domain a reference-typed argument is
150
- # dereferenced in
151
- # @param declaring [Bluebook::Aggregate, Bluebook::Entity, nil] the construct
152
- # `command` is declared on; also checks `enforce_lifecycle_guard` when given
153
- # @param parent [Runtime::Instance, nil] an entity command's own parent
154
- # aggregate record, readable as `parent.*`; nil for an aggregate command
155
- # @param correction [Hash{Symbol => Object}] the `{as_name => payload}`
156
- # bindings a `corrects` command's located old event offers
57
+ # @param subject [Runtime::Instance] pre-mutation record a `given` reads
58
+ # @param declaring [Bluebook::Aggregate, Bluebook::Entity, nil] also runs
59
+ # the lifecycle guard when given
60
+ # @param parent [Runtime::Instance, nil] entity command's parent record
61
+ # @param correction [Hash{Symbol => Object}] `corrects` event bindings
157
62
  # @return [void]
158
- # @raise [Runtime::GivenNotMet] if a declared `given` does not hold
159
- # @raise [Runtime::LifecycleRefused] if `declaring` is given and the command's
160
- # own `from:` guard refuses the record's current lifecycle state
161
- # @raise [Bluebook::Expression::EvaluationError] if a given's own rule cannot
162
- # be evaluated (an unresolvable field, a bad comparison)
63
+ # @raise [Runtime::GivenNotMet] a declared `given` does not hold
64
+ # @raise [Runtime::LifecycleRefused] `declaring`'s `from:` guard refuses
65
+ # the current lifecycle state
163
66
  def enforce_givens(subject, command, args, domain:, declaring: nil, parent: nil, correction: {})
164
67
  state = GuardState.new(subject)
165
- # A rule may only read within its own aggregate boundary (S12,
166
- # ADR 0025) — `subject`'s own stored references are not
167
- # dereferenced here at all: a `projects :customer_status, from:
168
- # :"customer.status"` field is just `subject`'s own
169
- # field, a regular stored attribute, already present in
170
- # `subject`/`state` with no hydration step needed. `dereference`
171
- # is still called on `command`/`args`, below — that is a
172
- # different case the ADR explicitly keeps in bounds ("its command
173
- # arguments"): a reference-typed argument this dispatch was just
174
- # handed (`Dispute`'s own `disputed_by`, say) has nothing stored
175
- # to project yet, so resolving it here, once, synchronously with
176
- # this command's own admission, is not the live-query-against-
177
- # another-aggregate's-stored-state pattern the boundary rule
178
- # forbids.
68
+ # A rule reads only within its own aggregate boundary (ADR 0025);
69
+ # `dereference` below resolves fresh reference arguments only,
70
+ # never a stored `projects` field, which `state` already has.
179
71
  attrs = args.merge(dereference(domain, command, args))
180
72
  attrs = attrs.merge(parent: parent.state) if parent
181
73
  attrs = attrs.merge(correction) unless correction.empty?
@@ -191,90 +83,38 @@ module Hecks
191
83
  enforce_lifecycle_guard(declaring, command, subject) if declaring
192
84
  end
193
85
 
194
- # `corrects` — CommandBuilder#corrects_impl's own comment. Not
195
- # expressible as an ordinary `given`: "has this exact record
196
- # already emitted this exact event" is not a predicate over the
197
- # record's own fields, it is a fact about the event log, so it is
198
- # raised structurally here, the same way NotFound/AlreadyExists
199
- # are, rather than through the expression evaluator. The build-
200
- # time half — does anything in this aggregate ever emit the named
201
- # event at all — is `AggregateBuilder#seal_correction_targets`;
202
- # this is the dispatch-time half — has this record actually done
203
- # so yet.
204
- #
205
- # Also locates the matched event now, not just its existence, and
206
- # returns a `{as_name => payload}` bindings hash — one entry per
207
- # `:corrects` mutation that named an `as:` — so `given`/`ensures`
208
- # on a corrects-bearing command can reference the located old
209
- # event by that name, the same shape `enforce_ensures`'s own
210
- # `old:` binding already has (CommandBuilder#corrects_impl's own
211
- # comment: `as:` was stored, from the start, specifically to be
212
- # wired into the evaluator once a real runtime consumer existed —
213
- # this is that consumer). `.reverse.find` — the most recent
214
- # matching event, if this record has somehow emitted the same
215
- # correction target more than once; the prior existence-only
216
- # check never had to make this choice, so it's a genuinely new
217
- # one, made deliberately: `as:` reads as "the instance being
218
- # corrected," which is naturally the latest fact on record, not
219
- # an arbitrary one.
220
- # Reads the durable event history a `corrects` mutation is judged against.
221
- #
222
- # C9.2 (docs/semantics/bluebook-semantics.md) — a correction target
223
- # is judged against the record's durable history: the events the
224
- # aggregate's own store recorded (`AppendOnly#events`), which
225
- # survive a restart the way the Rust kernel's persisted
226
- # `emitted_<event>` flag does. The in-process log is the fallback
227
- # only for an adapter that records no readable history.
86
+ # Reads the durable event history a `corrects` mutation is judged
87
+ # against (C9.2), scoped to the one record being corrected so a
88
+ # durable adapter need not load every aggregate's events to check
89
+ # one; falls back to the in-process log when needed.
228
90
  #
229
91
  # @param domain [String, Symbol] the domain `aggregate` belongs to
230
92
  # @param aggregate [Bluebook::Aggregate] the aggregate whose repository is read
231
- # @return [Array<Runtime::Event>] the aggregate's own durably recorded events
232
- # if its repository keeps a readable log, otherwise the registry's in-process
233
- # event log
234
- def correction_history(domain, aggregate)
235
- @registry.repository(domain, aggregate).events || @registry.event_log
93
+ # @param event_key [String] the `"domain::AggregateName"` key events are stored under
94
+ # @param id [String, Object] the record's identity being corrected
95
+ # @return [Array<Runtime::Event>] the record's recorded events, or the
96
+ # registry's in-process log as a fallback
97
+ def correction_history(domain, aggregate, event_key, id)
98
+ repository_events = @registry.repository(domain, aggregate).events_for(aggregate: event_key, id: id)
99
+ return repository_events if repository_events
100
+
101
+ # The in-process log holds every domain's events, so the fallback
102
+ # scopes it the same way a durable adapter's own query would.
103
+ @registry.event_log.select { |event| event.aggregate == event_key && event.id.to_s == id.to_s }
236
104
  end
237
105
 
238
- # Locates each `:corrects` mutation's own already-emitted target event, and
239
- # binds every `as:`-named one for `given`/`ensures` to reference.
240
- #
241
- # `corrects` — CommandBuilder#corrects_impl's own comment. Not
242
- # expressible as an ordinary `given`: "has this exact record
243
- # already emitted this exact event" is not a predicate over the
244
- # record's own fields, it is a fact about the event log, so it is
245
- # raised structurally here, the same way NotFound/AlreadyExists
246
- # are, rather than through the expression evaluator. The build-
247
- # time half — does anything in this aggregate ever emit the named
248
- # event at all — is `AggregateBuilder#seal_correction_targets`;
249
- # this is the dispatch-time half — has this record actually done
250
- # so yet.
251
- #
252
- # Also locates the matched event now, not just its existence, and
253
- # returns a `{as_name => payload}` bindings hash — one entry per
254
- # `:corrects` mutation that named an `as:` — so `given`/`ensures`
255
- # on a corrects-bearing command can reference the located old
256
- # event by that name, the same shape `enforce_ensures`'s own
257
- # `old:` binding already has (CommandBuilder#corrects_impl's own
258
- # comment: `as:` was stored, from the start, specifically to be
259
- # wired into the evaluator once a real runtime consumer existed —
260
- # this is that consumer). `.reverse.find` — the most recent
261
- # matching event, if this record has somehow emitted the same
262
- # correction target more than once; the prior existence-only
263
- # check never had to make this choice, so it's a genuinely new
264
- # one, made deliberately: `as:` reads as "the instance being
265
- # corrected," which is naturally the latest fact on record, not
266
- # an arbitrary one.
106
+ # Locates each `:corrects` mutation's already-emitted target event
107
+ # and binds every `as:`-named one for `given`/`ensures` to reference.
108
+ # Not an ordinary `given`: this is a log fact, raised structurally,
109
+ # like NotFound/AlreadyExists.
267
110
  #
268
111
  # @param instance [Runtime::Instance] the record being corrected
269
112
  # @param aggregate [Bluebook::Aggregate] the aggregate `instance` belongs to
270
- # @param command [Class] the command class (`Bluebook::Command` subclass)
271
- # whose `:corrects` mutations are located
113
+ # @param command [Class] the command whose `:corrects` mutations are located
272
114
  # @param domain [String, Symbol] the domain `aggregate` belongs to
273
- # @return [Hash{Symbol => Object}] the located event's payload, keyed by
274
- # each `:corrects` mutation's own `as:` name; empty for a mutation with
275
- # no `as:`
276
- # @raise [Runtime::NothingToCorrect] if a `:corrects` mutation names an
277
- # event `instance` has never emitted
115
+ # @return [Hash{Symbol => Object}] payload per mutation's `as:` name; empty
116
+ # for a mutation with no `as:`
117
+ # @raise [Runtime::NothingToCorrect] a named event `instance` never emitted
278
118
  def enforce_correction_target(instance, aggregate, command, domain:)
279
119
  bindings = {}
280
120
  command.mutations.each do |mutation|
@@ -282,8 +122,11 @@ module Hecks
282
122
 
283
123
  event_key = "#{domain}::#{aggregate.hecks_name}"
284
124
  event_name = mutation.target.to_s
285
- corrected = correction_history(domain, aggregate).reverse.find do |event|
286
- event.name == event_name && event.aggregate == event_key && event.id.to_s == instance.id.to_s
125
+ # Most recent match wins if this record emitted the same event
126
+ # more than once; aggregate and id are already scoped by
127
+ # correction_history, so only the event name is filtered here.
128
+ corrected = correction_history(domain, aggregate, event_key, instance.id).reverse.find do |event|
129
+ event.name == event_name
287
130
  end
288
131
 
289
132
  unless corrected
@@ -298,25 +141,16 @@ module Hecks
298
141
  bindings
299
142
  end
300
143
 
301
- # Lifecycle state as a command guard (S10, ADR 0025) — `command
302
- # "Debit", from: "open"` checked here, folded into the same
303
- # dispatch step `given` already runs at (both are preconditions,
304
- # evaluated before any mutation) rather than earning its own
305
- # DISPATCH_ORDER entry. A guard, never a transition: it names no
306
- # target state and `step_advance_lifecycle` never sees it — see
307
- # `admissible_transition`, right below, for the transition this
308
- # is deliberately not reusing (its own `StateTransition#target`
309
- # is required, and a guard-only command has none to give it).
144
+ # Checks a command's own `from:` lifecycle guard — a precondition,
145
+ # not a transition; `admissible_transition` below handles moves.
310
146
  #
311
- # @param declaring [Bluebook::Aggregate, Bluebook::Entity] the construct
312
- # whose lifecycle field is checked
313
- # @param command [Class] the command class (`Bluebook::Command` subclass)
314
- # whose `from:` guard is checked
315
- # @param subject [Runtime::Instance] the pre-mutation record to read the
316
- # current lifecycle state off
147
+ # @param declaring [Bluebook::Aggregate, Bluebook::Entity] construct whose
148
+ # lifecycle field is checked
149
+ # @param command [Class] the command class whose `from:` guard is checked
150
+ # @param subject [Runtime::Instance] pre-mutation record to read lifecycle off
317
151
  # @return [void]
318
- # @raise [Runtime::LifecycleRefused] if `command` declares `from:` and the
319
- # record's current lifecycle state is not one of them
152
+ # @raise [Runtime::LifecycleRefused] `command` declares `from:` and current
153
+ # state isn't one of them
320
154
  def enforce_lifecycle_guard(declaring, command, subject)
321
155
  return unless command.from
322
156
 
@@ -324,15 +158,8 @@ module Hecks
324
158
  current = Value.scalar(subject[lifecycle.field]).to_s
325
159
  return if Array(command.from).include?(current)
326
160
 
327
- # Routed through RefusalWording's own "transition_blocked"
328
- # template — the same one #admissible_transition, right below,
329
- # already raises LifecycleRefused through for the same
330
- # refusal class, rather than hand-rolling its own wording
331
- # inline ("...only runs from..." vs. the template's "...moves
332
- # it only from..."), two shapes for one refusal kind, so
333
- # anything string-matching a LifecycleRefused message (a
334
- # property, a spec, a caller) would have to know both existed
335
- # rather than one.
161
+ # Routed through the same "transition_blocked" template
162
+ # `#admissible_transition` uses, so both raise identical wording.
336
163
  raise LifecycleRefused,
337
164
  RefusalWording.render_site("LifecycleRefused", "transition_blocked",
338
165
  command: command.hecks_name, field: lifecycle.field,
@@ -340,55 +167,21 @@ module Hecks
340
167
  allowed: Array(command.from))
341
168
  end
342
169
 
343
- # The far side of the contract: evaluated against the settled record
344
- # — after mutations and the lifecycle move, before anything persists
345
- # — with `old` carrying the state as the givens saw it. Injected into
346
- # the attrs at evaluation time only; the payload gate never sees it.
347
- #
348
- # `old` — and every dispatch argument — wins over a same-named state
349
- # field in expression scope (Resolver#fetch checks attrs first). An
350
- # ensures naming a field the command also takes as an argument (or,
351
- # on an entity, a field that doubles as the addressing argument
352
- # element_of reads) will read the argument, not the settled value.
353
- # Not new to ensures — `given` lives under the same rule — but an
354
- # ensures is more likely to collide, since it typically re-reads a
355
- # field the command just took in to mutate it.
170
+ # Checks `command`'s declared `ensures` against `subject`, the settled
171
+ # post-mutation record; `old` carries the pre-mutation state.
172
+ # An argument sharing a settled field's name does not shadow it here.
356
173
  #
357
- # @param subject [Runtime::Instance] the settled, post-mutation record an
358
- # `ensures` is checked against
359
- # @param command [Class] the command class (`Bluebook::Command` subclass)
360
- # whose declared `ensures` are checked
361
- # @param args [Hash{String, Symbol => Object}] the offered command arguments,
362
- # readable by an ensures unless `subject` shares the same field name
363
- # @param old [Hash{Symbol => Object}, nil] the pre-mutation state, readable
364
- # as `old.*`; nil when `command` declares no ensures (dup skipped upstream)
365
- # @param domain [String, Symbol] the domain a reference-typed argument is
366
- # dereferenced in
367
- # @param parent [Runtime::Instance, nil] an entity command's own parent
368
- # aggregate record, readable as `parent.*`; nil for an aggregate command
369
- # @param correction [Hash{Symbol => Object}] the `{as_name => payload}`
370
- # bindings a `corrects` command's located old event offers
174
+ # @param subject [Runtime::Instance] the settled, post-mutation record
175
+ # @param old [Hash{Symbol => Object}, nil] pre-mutation state, readable as `old.*`
176
+ # @param parent [Runtime::Instance, nil] entity command's parent record, if any
177
+ # @param correction [Hash{Symbol => Object}] `corrects` event bindings, if any
371
178
  # @return [void]
372
- # @raise [Runtime::EnsuresNotMet] if a declared `ensures` does not hold
373
- # @raise [Bluebook::Expression::EvaluationError] if an ensures's own rule
374
- # cannot be evaluated (an unresolvable field, a bad comparison)
179
+ # @raise [Runtime::EnsuresNotMet] a declared `ensures` does not hold
375
180
  def enforce_ensures(subject, command, args, old:, domain:, parent: nil, correction: {})
376
181
  state = GuardState.new(subject)
377
- # S12, ADR 0025 — same boundary reasoning as enforce_givens
378
- # above: `subject`'s own stored references are not
379
- # dereferenced here; a `projects`-maintained field is already
380
- # part of `state`. `command`/`args` still dereferences — a
381
- # fresh reference-typed argument stays in bounds.
382
- # `old` still wins over everything, unchanged. `correction`
383
- # (an `as:`-bound corrected event, if this command declares
384
- # one) wins right alongside it — a settled-record ensures can
385
- # reference the correction target exactly as freely as a
386
- # pre-mutation given already can.
387
- # C2.3 (docs/semantics/bluebook-semantics.md) — an ensures reads
388
- # the settled state first: an argument that shares a field's
389
- # name does not shadow the candidate here (it does in a given,
390
- # C2.2), so `sets :note` + `ensures { note == ... }` judges what
391
- # landed, and `old.<field>` remains the pre-state.
182
+ # Same aggregate-boundary rule as enforce_givens (ADR 0025) —
183
+ # `state` already carries projected fields; only a fresh
184
+ # reference-typed argument needs `dereference` here.
392
185
  attrs = args.reject { |name, _| subject.key?(name) }.merge(dereference(domain, command, args))
393
186
  attrs = attrs.merge(parent: parent.state) if parent
394
187
  attrs = attrs.merge(correction) unless correction.empty?
@@ -400,37 +193,18 @@ module Hecks
400
193
  end
401
194
  end
402
195
 
403
- # The aggregate boundary, checked after every command, before
404
- # save (S10, ADR 0025 — "Rules") — the same point `enforce_
405
- # ensures` already checks at, and for the same reason: an
406
- # invariant is a claim about the settled record, not the
407
- # command that produced it, so it reads no `args`/`old` at all,
408
- # only the record's own state. `subject` here is
409
- # always the aggregate's own instance — `CommandInterpreter`
410
- # passes its own `ctx.instance`, and `EntityInterpreter` passes
411
- # the parent record (`ctx.instance`, not the element), since an
412
- # entity mutation changes data inside the same aggregate
413
- # boundary the invariant guards; there is no separate "entity
414
- # invariant" to check the piece's own view against.
196
+ # Checks `aggregate`'s declared invariants against the settled
197
+ # `subject`, then each of its entities' own invariants.
415
198
  #
416
- # No `dereference` (S12, ADR 0025) — an invariant may only read
417
- # `subject`'s own boundary, same rule `enforce_givens`/
418
- # `enforce_ensures` now hold to. No invariant in the corpus has
419
- # ever read across a `reference_to` (verified by grep), so this
420
- # is not a migration, just closing the same capability off here
421
- # that was already unused.
199
+ # No `dereference` here (ADR 0025) — an invariant may only read
200
+ # `subject`'s own boundary, same rule enforce_givens/ensures hold to.
422
201
  #
423
- # @param subject [Runtime::Instance] the settled aggregate record an
424
- # invariant is checked against
425
- # @param aggregate [Bluebook::Aggregate] the aggregate whose declared
426
- # `invariants` are checked
427
- # @param domain [String, Symbol] the domain `aggregate` belongs to, passed
428
- # through to `check_entity_invariants`
202
+ # @param subject [Runtime::Instance] the settled aggregate record checked
203
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose invariants are checked
204
+ # @param domain [String, Symbol] domain `aggregate` belongs to
429
205
  # @return [void]
430
- # @raise [Runtime::InvariantViolation] if a declared invariant does not hold,
431
- # on `aggregate` itself or any of its entities
432
- # @raise [Bluebook::Expression::EvaluationError] if an invariant's own rule
433
- # cannot be evaluated (an unresolvable field, a bad comparison)
206
+ # @raise [Runtime::InvariantViolation] a declared invariant does not hold, on
207
+ # `aggregate` itself or any of its entities
434
208
  def enforce_invariants(subject, aggregate, domain:)
435
209
  state = GuardState.new(subject)
436
210
  attrs = {}
@@ -443,38 +217,17 @@ module Hecks
443
217
  check_entity_invariants(aggregate, subject, domain: domain)
444
218
  end
445
219
 
446
- # A piece's own shape rule, checked against every instance the
447
- # aggregate holds — not a separate boundary from the aggregate's
448
- # own invariants just above (same two checkpoints: after every
449
- # mutation, before save), just a wider one: the aggregate's own
450
- # consistency includes each of its pieces individually looking
451
- # right, the same way `ValueObject#invariants` already checks
452
- # each of its own instances one construct up. Recurses into
453
- # nested pieces (S17, ADR 0026 — Dispatch inside Handler) the
454
- # same way `check_entity_invariants`'s own caller recurses
455
- # nowhere else needs to, since a piece's `entities` are already
456
- # exactly as reachable as an aggregate's.
220
+ # Checks each of `owner_construct`'s entity types' own invariants
221
+ # against every instance it holds, recursing into nested entities.
457
222
  #
458
- # `list_attr` reuses the exact lookup `EntityInterpreter#
459
- # element_of` already makes to locate a single addressed
460
- # element by identity — this reads every element instead, but
461
- # the "which field on the owner holds this piece's own
462
- # instances" question is the identical one. A piece declaring
463
- # invariants that nothing on its owner actually holds (no
464
- # matching list attribute) is a static-analysis gap for a
465
- # future gate, not a runtime concern here — `next` past it
466
- # rather than raising mid-enforcement for an unrelated command.
223
+ # An entity with no matching list attribute on its owner is skipped,
224
+ # not raised — a static-analysis gap, not a runtime concern here.
467
225
  #
468
- # @param owner_construct [Bluebook::Aggregate, Bluebook::Entity] the
469
- # construct whose nested entities are checked
470
- # @param owner_instance [Runtime::Instance] the settled record holding
471
- # `owner_construct`'s own entity lists
226
+ # @param owner_construct [Bluebook::Aggregate, Bluebook::Entity] whose entities are checked
227
+ # @param owner_instance [Runtime::Instance] settled record holding the entity lists
472
228
  # @param domain [String, Symbol] the domain `owner_construct` belongs to
473
229
  # @return [void]
474
- # @raise [Runtime::InvariantViolation] if a declared entity invariant does
475
- # not hold, on any element or its own nested entities
476
- # @raise [Bluebook::Expression::EvaluationError] if an invariant's own rule
477
- # cannot be evaluated (an unresolvable field, a bad comparison)
230
+ # @raise [Runtime::InvariantViolation] an entity invariant does not hold
478
231
  def check_entity_invariants(owner_construct, owner_instance, domain:)
479
232
  owner_construct.entities.each do |entity|
480
233
  next if entity.invariants.empty?
@@ -485,7 +238,7 @@ module Hecks
485
238
  Array(owner_instance[list_attr.name]).each do |element|
486
239
  wrapped = Instance.new(aggregate: entity, id: nil, state: element)
487
240
  element_state = GuardState.new(wrapped)
488
- # No `dereference` (S12, ADR 0025) — same boundary rule as
241
+ # No `dereference` (ADR 0025) — same boundary rule as
489
242
  # enforce_invariants above; `parent` (the owner's own
490
243
  # state, projected fields included) stays readable.
491
244
  attrs = { parent: owner_instance.state }
@@ -501,20 +254,16 @@ module Hecks
501
254
  end
502
255
  end
503
256
 
504
- # Finds the lifecycle transition `command` admits from `subject`'s current
505
- # state, if `declaring` declares a lifecycle and `command` moves it.
257
+ # Finds the lifecycle transition `command` admits from `subject`'s
258
+ # current state, if `declaring` declares a lifecycle and moves it.
506
259
  #
507
- # @param declaring [Bluebook::Aggregate, Bluebook::Entity] the construct
508
- # whose lifecycle is checked
509
- # @param command [Class] the command class (`Bluebook::Command` subclass)
510
- # to find a declared transition for
511
- # @param subject [Runtime::Instance] the pre-mutation record to read the
512
- # current lifecycle state off
513
- # @return [Bluebook::StateTransition, nil] the admitted transition, or nil
514
- # when `declaring` has no lifecycle or `command` declares no transition
515
- # @raise [Runtime::LifecycleRefused] if `command` declares one or more
516
- # transitions, each constrained by its own `from:`, and none admits the
517
- # record's current lifecycle state
260
+ # @param declaring [Bluebook::Aggregate, Bluebook::Entity] the construct with the lifecycle
261
+ # @param command [Class] the command class to find a declared transition for
262
+ # @param subject [Runtime::Instance] pre-mutation record read for current state
263
+ # @return [Bluebook::StateTransition, nil] admitted transition, or nil when
264
+ # `declaring` has no lifecycle or `command` declares none
265
+ # @raise [Runtime::LifecycleRefused] `command` declares transitions and none
266
+ # admits the record's current state
518
267
  def admissible_transition(declaring, command, subject)
519
268
  lifecycle = declaring.lifecycle
520
269
  return nil unless lifecycle
@@ -522,28 +271,8 @@ module Hecks
522
271
  candidates = lifecycle.transitions_for(command.hecks_name)
523
272
  return nil if candidates.empty?
524
273
 
525
- # `Value.scalar` unwrap -- vendored addition, not (yet)
526
- # upstream hecks (migration plan task 9): a VO-typed
527
- # lifecycle field (the norm, not the exception, per this
528
- # corpus's own no-primitive-envy convention) holds a real
529
- # `Runtime::Value` here, and a bare `.to_s` on that hit Ruby's
530
- # default `Object#to_s` instead of unwrapping the inner
531
- # scalar first -- `current` came back as a raw object-pointer
532
- # string (`"#<Hecks::Runtime::Value:0x...>"`) that could
533
- # never match any declared `from` state, so every transition
534
- # on a VO-typed lifecycle field refused unconditionally, and
535
- # when it refused the message leaked the pointer too.
536
- # Confirmed live via `Plan::Task.Complete` (status defaults to
537
- # `TaskStatus`, a single-field VO), not inferred. Reuses
538
- # `Value.scalar` -- this file's own third candidate for "how
539
- # to unwrap a Value/Hash-shaped field," already built and
540
- # already documented for exactly this job ("rendering a value
541
- # object into a column or a message, where there is no path
542
- # to consult," `value/coercion.rb`'s own comment) -- rather
543
- # than inventing a second unwrap helper beside `Resolver#
544
- # unwrap_scalar`'s bare-comparison one. Duck-typed the same
545
- # way : a bare, non-VO lifecycle field passes through
546
- # unchanged (`Value.scalar` only opens a `Value` instance).
274
+ # `Value.scalar` unwraps a VO-typed lifecycle field before `.to_s`;
275
+ # without it, a wrapped field never matches any `from:` state.
547
276
  current = Value.scalar(subject[lifecycle.field]).to_s
548
277
  admitted = candidates.find { |t| !t.constrained? || Array(t.from).include?(current) }
549
278
  return admitted if admitted